Repository: alienator88/Pearcleaner Branch: main Commit: 3222dc8f305a Files: 179 Total size: 15.0 MB Directory structure: gitextract_u4mjrmj6/ ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug.md │ │ ├── config.yml │ │ ├── feature.md │ │ └── project.md │ └── workflows/ │ └── issues.yml ├── .gitignore ├── Builds/ │ ├── ExportOptions.plist │ └── changes.md ├── FUNDING.yml ├── FinderOpen/ │ ├── Assets.xcassets/ │ │ ├── Contents.json │ │ └── Glass.imageset/ │ │ └── Contents.json │ ├── FinderOpen.entitlements │ ├── FinderOpen.swift │ ├── Info.plist │ └── Localizable.xcstrings ├── LICENSE.md ├── Pear Resources/ │ ├── Glass.icon/ │ │ ├── Assets/ │ │ │ └── pear.heic │ │ └── icon.json │ └── Pear.psd ├── Pearcleaner/ │ ├── Logic/ │ │ ├── AppCommands.swift │ │ ├── AppInfoFetch.swift │ │ ├── AppPathsFetch.swift │ │ ├── AppState.swift │ │ ├── AppsUpdater/ │ │ │ ├── AppStoreReset.swift │ │ │ ├── AppStoreUpdateChecker.swift │ │ │ ├── AppStoreUpdater.swift │ │ │ ├── HomebrewAdoption.swift │ │ │ ├── HomebrewUpdateChecker.swift │ │ │ ├── IOSAppInstaller.swift │ │ │ ├── Models.swift │ │ │ ├── PrivateFrameworks/ │ │ │ │ ├── CFBundle/ │ │ │ │ │ └── CFBundle_Private.h │ │ │ │ ├── CommerceKit/ │ │ │ │ │ ├── CKDownloadDirectory.h │ │ │ │ │ ├── CKDownloadQueue.h │ │ │ │ │ ├── CKDownloadQueueObserver-Protocol.h │ │ │ │ │ ├── CKPurchaseController.h │ │ │ │ │ ├── CKServiceInterface.h │ │ │ │ │ ├── CommerceKit.h │ │ │ │ │ └── module.modulemap │ │ │ │ └── StoreFoundation/ │ │ │ │ ├── ISAccountService-Protocol.h │ │ │ │ ├── ISServiceProxy.h │ │ │ │ ├── ISStoreAccount.h │ │ │ │ ├── SSDownload.h │ │ │ │ ├── SSDownloadMetadata.h │ │ │ │ ├── SSDownloadPhase.h │ │ │ │ ├── SSDownloadStatus.h │ │ │ │ ├── SSPurchase.h │ │ │ │ ├── SSPurchaseResponse.h │ │ │ │ ├── StoreFoundation.h │ │ │ │ └── module.modulemap │ │ │ ├── SSPurchase+Extension.swift │ │ │ ├── SparkleUpdateChecker.swift │ │ │ ├── SparkleUpdateDriver.swift │ │ │ ├── SparkleUpdateOperation.swift │ │ │ ├── UpdateManager.swift │ │ │ ├── UpdateQueue.swift │ │ │ ├── UpdaterDebugLogger.swift │ │ │ ├── UpdaterSettings.swift │ │ │ └── VersionComparison.swift │ │ ├── Brew/ │ │ │ ├── HomebrewAutoUpdateManager.swift │ │ │ ├── HomebrewController.swift │ │ │ ├── HomebrewManager.swift │ │ │ ├── HomebrewPackage.swift │ │ │ ├── HomebrewTap.swift │ │ │ └── HomebrewUninstaller.swift │ │ ├── CLI.swift │ │ ├── Conditions.swift │ │ ├── DeepLink.swift │ │ ├── FileSearch/ │ │ │ ├── FileSearchLogic.swift │ │ │ └── FileSearchModels.swift │ │ ├── FuzzySearch.swift │ │ ├── GlobalConsoleManager.swift │ │ ├── HelperToolManager.swift │ │ ├── KeychainPasswordManager.swift │ │ ├── Lipo.swift │ │ ├── Locations.swift │ │ ├── Logic.swift │ │ ├── PKG/ │ │ │ ├── PKBOM.h │ │ │ ├── PKBundleComponent.h │ │ │ ├── PKBundleComponentVersion.h │ │ │ ├── PKComponent.h │ │ │ ├── PKGManager.swift │ │ │ ├── PKInstallHistory.h │ │ │ ├── PKPackage.h │ │ │ ├── PKPackageChecker.h │ │ │ ├── PKPackageInfo.h │ │ │ ├── PKProductInfo.h │ │ │ ├── PKReceipt.h │ │ │ └── Pearcleaner-Bridging-Header.h │ │ ├── PasswordRequestHandler.swift │ │ ├── ProcessEnv.swift │ │ ├── ReversePathsFetch.swift │ │ ├── TCC/ │ │ │ ├── TCCModels.swift │ │ │ └── TCCQueryHelper.swift │ │ ├── UndoHistoryManager.swift │ │ ├── UndoManager.swift │ │ └── Utilities.swift │ ├── PearcleanerApp.swift │ ├── Resources/ │ │ ├── Assets.xcassets/ │ │ │ ├── AccentColor.colorset/ │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── Glass.icon/ │ │ │ ├── Assets/ │ │ │ │ └── pear.heic │ │ │ └── icon.json │ │ ├── Info.plist │ │ ├── Localizable.xcstrings │ │ ├── Pearcleaner.entitlements │ │ └── askpass.sh │ ├── Style/ │ │ ├── ChromeBorder.swift │ │ ├── CircularProgressView.swift │ │ ├── ControlGroupChrome.swift │ │ ├── PearGroupBox.swift │ │ ├── SparkleProgressBar.swift │ │ ├── Styles.swift │ │ └── Theme.swift │ ├── Views/ │ │ ├── AppsUpdaterView/ │ │ │ ├── AdoptionSheetView.swift │ │ │ ├── AppsUpdaterView.swift │ │ │ ├── CaskAdoptionContentView.swift │ │ │ ├── ExpandableActionButton.swift │ │ │ ├── UpdateDetailView.swift │ │ │ ├── UpdateRowViewSidebar.swift │ │ │ └── UpdaterDetailsSidebar.swift │ │ ├── AppsView/ │ │ │ ├── AppListItems.swift │ │ │ ├── AppSearchView.swift │ │ │ ├── AppsListView.swift │ │ │ └── GridAppItem.swift │ │ ├── Brew/ │ │ │ ├── AutoUpdateSection.swift │ │ │ ├── HomebrewView.swift │ │ │ ├── LogViewerSheet.swift │ │ │ ├── MaintenanceSection.swift │ │ │ ├── PackageDetailsSidebar.swift │ │ │ ├── SearchInstallSection.swift │ │ │ └── TapManagementSection.swift │ │ ├── Components/ │ │ │ ├── BadgeOverlay.swift │ │ │ ├── GlobalConsoleView.swift │ │ │ ├── PermissionsSheetView.swift │ │ │ ├── SidebarDetailView/ │ │ │ │ ├── GenericSidebarListView.swift │ │ │ │ └── SidebarDetailLayout.swift │ │ │ ├── StandardSheetView.swift │ │ │ └── TCCPermissionViewer.swift │ │ ├── DaemonView.swift │ │ ├── DeleteHistoryView.swift │ │ ├── DevelopmentView.swift │ │ ├── FileSearchView.swift │ │ ├── FilesView/ │ │ │ ├── FileCategory.swift │ │ │ ├── FileListView.swift │ │ │ ├── FilesSidebarView.swift │ │ │ ├── FilesView.swift │ │ │ └── TranslationSelectionSheet.swift │ │ ├── LipoView/ │ │ │ ├── LipoSidebarView.swift │ │ │ └── LipoView.swift │ │ ├── MainWindow.swift │ │ ├── PackageView.swift │ │ ├── PluginsView.swift │ │ ├── Settings/ │ │ │ ├── About.swift │ │ │ ├── Folders.swift │ │ │ ├── General.swift │ │ │ ├── Helper.swift │ │ │ ├── Interface.swift │ │ │ ├── SettingsWindow.swift │ │ │ └── Update.swift │ │ └── ZombieView/ │ │ ├── ZombieSidebarView.swift │ │ └── ZombieView.swift │ └── announcements.json ├── Pearcleaner.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm/ │ │ └── Package.resolved │ └── xcshareddata/ │ └── xcschemes/ │ ├── FinderOpen.xcscheme │ ├── Pearcleaner Debug.xcscheme │ ├── Pearcleaner Release.xcscheme │ ├── PearcleanerSentinel Release.xcscheme │ └── PearcleanerSentinel.xcscheme ├── PearcleanerHelper/ │ ├── CodesignCheck.swift │ ├── com.alienator88.Pearcleaner.PearcleanerHelper.plist │ └── main.swift ├── PearcleanerSentinel/ │ ├── FileWatcher.swift │ ├── com.alienator88.PearcleanerSentinel.plist │ └── main.swift ├── README.md ├── Shared/ │ └── AppGroupDefaults.swift └── announcements.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/ISSUE_TEMPLATE/bug.md ================================================ --- name: 🐛 Bug Report about: For submitting new bugs related to the application functionality title: "[BUG] ENTER ISSUE TITLE HERE" labels: 'bug' assignees: '' --- ### New Issue Checklist - [ ] I updated Pearcleaner to the latest version and still observe the issue - [ ] I searched for [existing GitHub issues](https://github.com/alienator88/pearcleaner/issues) - [ ] OS Version: [e.g. 13.0] - [ ] Pearcleaner Version: [e.g. 3.x.x] --- ### Issue: #### Steps: #### Screenshots: #### Debug Console
 [REPLACE THIS TEXT BETWEEN THE PRE TAGS WITH YOUR LOGS] 
--- #### Console Logs
 [REPLACE THIS TEXT BETWEEN THE PRE TAGS WITH YOUR LOGS] 
================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: PEARCLEANER IS NOW IN MAINTENANCE MODE url: https://github.com/alienator88/Pearcleaner/blob/main/.github/ISSUE_TEMPLATE/project.md about: New feature requests will be ignored/closed. Click here for more details. - name: ❓ Ask a Question url: https://github.com/alienator88/Pearcleaner/discussions/new?category=general about: Ask the community for help ================================================ FILE: .github/ISSUE_TEMPLATE/feature.md ================================================ ### New Feature Checklist - [ ] I searched for existing open/closed feature requests and found none related to this request - [ ] My request is not design/opinion based - [ ] My request will be beneficial to most users --- ### Request: #### Desired Solution: #### Additional Context: ================================================ FILE: .github/ISSUE_TEMPLATE/project.md ================================================ This project is now in **maintenance mode**. --- I’m no longer accepting **NEW** feature requests as Pearcleaner has grown to be fairly feature rich, and I want to avoid it ballooning further in size or scope. Keeping the project stable, focused, and maintainable is now the priority. Going forward, the focus will be on: - Bug fixes - Regressions - Small improvements to already existing features Issues related to bugs are still welcome, and reasonable pull requests that focus on fixes or reliability improvements will still be reviewed. New feature requests will be ignored/closed if submitted. Thanks to everyone who has used the project, filed issues, and contributed over time! ================================================ FILE: .github/workflows/issues.yml ================================================ name: Close empty issues and templates on: issues: types: - reopened - opened - edited jobs: closeEmptyIssuesAndTemplates: name: Close empty issues and templates runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 # NOTE: Retrieve issue templates. - name: Run empty issues closer action uses: rickstaa/empty-issues-closer-action@v1 env: github_token: ${{ secrets.GITHUB_TOKEN }} with: close_comment: Closing this issue because it appears to be empty. Please update the issue for it to be reopened. open_comment: Reopening this issue because the author provided more information. check_templates: true template_close_comment: Closing this issue since the issue template was not filled in. Please provide us with more information to have this issue reopened. template_open_comment: Reopening this issue because the author provided more information. ================================================ FILE: .gitignore ================================================ # Xcode # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore ## User settings xcuserdata/ ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) *.xcscmblueprint *.xccheckout ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) build/ DerivedData/ *.moved-aside *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 ## Obj-C/Swift specific *.hmap ## App packaging *.ipa *.dSYM.zip *.dSYM ## Playgrounds timeline.xctimeline playground.xcworkspace # Swift Package Manager # # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. # Packages/ # Package.pins # Package.resolved # *.xcodeproj # # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata # hence it is not needed unless you have added a package configuration file to your project # .swiftpm .build/ # CocoaPods # # We recommend against adding the Pods directory to your .gitignore. However # you should judge for yourself, the pros and cons are mentioned at: # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control # # Pods/ # # Add this line if you want to avoid checking in source code from the Xcode workspace # *.xcworkspace # Carthage # # Add this line if you want to avoid checking in source code from Carthage dependencies. # Carthage/Checkouts Carthage/Build/ # Accio dependency management Dependencies/ .accio/ # fastlane # # It is recommended to not store the screenshots in the git repo. # Instead, use fastlane to re-generate the screenshots whenever they are needed. # For more information about the recommended setup visit: # https://docs.fastlane.tools/best-practices/source-control/#source-control fastlane/report.xml fastlane/Preview.html fastlane/screenshots/**/*.png fastlane/test_output # Code Injection # # After new code Injection tools there's a generated folder /iOSInjectionProject # https://github.com/johnno1962/injectionforxcode iOSInjectionProject/ .DS_Store Builds/Export .claude Pearcleaner/Resources/Localizable.xcstrings.bak ================================================ FILE: Builds/ExportOptions.plist ================================================ method mac-application teamID BK8443AXLU signingCertificate Developer ID Application: Marius Lupascu (BK8443AXLU) signingStyle manual stripSwiftSymbols destination export uploadSymbols manageAppVersionAndBuildNumber thinning <none> embedOnDemandResourcesAssetPacksInBundle generateAppStoreInformation testFlightInternalTestingOnly ================================================ FILE: Builds/changes.md ================================================ ### What's New - [x] Ability to hide unused utility pages from global menu in Settings > Interface > Startup View - #476 - [x] "Update All" button now shows progress in toolbar for Updater view - [x] Add pinyin sorting to homebrew view and updater view - #479 - [x] Warn if updating MAS app with a different source as it can break MAS connection - #484 ### Fixes - [x] Fix pear list-orphaned cli command - #472 - [x] Fix owner of receipt to root:wheel for installd workaround - #475 - [x] Add contentShape around sidebar toggle icons - #485 - [x] Fix computed property not refreshing action buttons in some cases for Updater view - [x] Reorder some layout components in Updater view - [x] Translations ================================================ FILE: FUNDING.yml ================================================ github: alienator88 ================================================ FILE: FinderOpen/Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: FinderOpen/Assets.xcassets/Glass.imageset/Contents.json ================================================ { "images" : [ { "filename" : "Glass.png", "idiom" : "universal", "scale" : "1x" }, { "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: FinderOpen/FinderOpen.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.application-groups group.com.alienator88.Pearcleaner com.apple.security.temporary-exception.files.absolute-path.read-only / ================================================ FILE: FinderOpen/FinderOpen.swift ================================================ // // FinderSync.swift // FinderOpen // // Created by Alin Lupascu on 4/11/24. // import Cocoa import FinderSync class FinderOpen: FIFinderSync { override init() { super.init() NSLog("FinderSync() launched from %@", Bundle.main.bundlePath as NSString) // Set the directory URLs that the Finder Sync extension observes FIFinderSyncController.default().directoryURLs = Set([URL(fileURLWithPath: "/")]) } override func menu(for menuKind: FIMenuKind) -> NSMenu { let menu = NSMenu(title: "") // Ensure we are dealing with the contextual menu for items if menuKind == .contextualMenuForItems { // Get the selected items if let selectedItemURLs = FIFinderSyncController.default().selectedItemURLs(), selectedItemURLs.count == 1, selectedItemURLs.first?.pathExtension == "app" { // Add menu item if the selected item is a .app file let menuItem = NSMenuItem(title: String(localized: "Pearcleaner Uninstall"), action: #selector(openInMyApp), keyEquivalent: "") // Add icon if enabled in main app if UserDefaults.showAppIconInMenu { if let appIcon = NSApp.applicationIconImage { appIcon.size = NSSize(width: 16, height: 16) menuItem.image = appIcon } else if let fallbackIcon = NSImage(named: "Glass") { fallbackIcon.size = NSSize(width: 16, height: 16) menuItem.image = fallbackIcon } } menu.addItem(menuItem) } } // Return the menu (which may be empty if the conditions are not met) return menu } @objc func openInMyApp(_ sender: AnyObject?) { // Get the selected items (files/folders) in Finder guard let selectedItems = FIFinderSyncController.default().selectedItemURLs(), !selectedItems.isEmpty else { return } // Consider only the first selected item let firstSelectedItem = selectedItems[0] let path = firstSelectedItem.path NSWorkspace.shared.open(URL(string: "pear://com.alienator88.Pearcleaner?path=\(path)")!) } } ================================================ FILE: FinderOpen/Info.plist ================================================ NSExtension NSExtensionAttributes NSExtensionPointIdentifier com.apple.FinderSync NSExtensionPrincipalClass $(PRODUCT_MODULE_NAME).FinderOpen ================================================ FILE: FinderOpen/Localizable.xcstrings ================================================ { "sourceLanguage" : "en", "strings" : { "Pearcleaner Uninstall" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner Deinstallation" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalar Pearcleaner" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Désinstaller avec Pearcleaner" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus Instalasi Pearlcleaner" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleanerでアンインストール" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Odinstaluj za pomocą Pearcleaner" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalar o Pearcleaner" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить через Pearcleaner" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odinštalovať Pearcleanerom" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner ile Kaldırma" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити через Pearcleaner" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Gỡ cài đặt Pearcleaner" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner 卸载" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner 解除安裝" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner 解除安裝" } } } } }, "version" : "1.0" } ================================================ FILE: LICENSE.md ================================================ “Commons Clause” License Condition v1.0 The Software is provided to you by the Licensor under the License, as defined below, subject to the following condition. Without limiting other conditions in the License, the grant of rights under the License will not include, and the License does not grant to you, the right to Sell the Software. For purposes of the foregoing, “Sell” means practicing any or all of the rights granted to you under the License to provide to third parties, for a fee or other consideration (including without limitation fees for hosting or consulting/ support services related to the Software), a product or service whose value derives, entirely or substantially, from the functionality of the Software. Any license notice or attribution required by the License must also include this Commons Clause License Condition notice. Software: Pearcleaner License: Apache 2.0 with Commons Clause Licensor: alienator88 --- Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Pear Resources/Glass.icon/icon.json ================================================ { "fill" : { "linear-gradient" : [ "display-p3:0.93159,0.94081,0.94081,1.00000", "display-p3:0.75900,0.75900,0.75900,1.00000" ] }, "groups" : [ { "layers" : [ { "blend-mode" : "normal", "glass-specializations" : [ { "value" : false }, { "appearance" : "dark", "value" : true }, { "appearance" : "tinted", "value" : true } ], "hidden" : false, "image-name" : "pear.heic", "name" : "pear", "position" : { "scale" : 1.5, "translation-in-points" : [ 11.3046875, 3.296875 ] } } ], "shadow" : { "kind" : "neutral", "opacity" : 0.5 }, "translucency" : { "enabled" : true, "value" : 0.5 } } ], "supported-platforms" : { "squares" : [ "macOS" ] } } ================================================ FILE: Pear Resources/Pear.psd ================================================ [File too large to display: 10.7 MB] ================================================ FILE: Pearcleaner/Logic/AppCommands.swift ================================================ // // AppCommands.swift // Pearcleaner // // Created by Alin Lupascu on 10/31/23. // import SwiftUI import AlinFoundation struct AppCommands: Commands { let appState: AppState let locations: Locations let fsm: FolderSettingsManager let updater: Updater @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true @AppStorage("settings.general.selectedTab") private var selectedTab: CurrentTabView = .general @State private var windowController = WindowManager() @ObservedObject private var debugLogger = UpdaterDebugLogger.shared init(appState: AppState, locations: Locations, fsm: FolderSettingsManager, updater: Updater) { self.appState = appState self.locations = locations self.fsm = fsm self.updater = updater } var body: some Commands { // Pearcleaner Menu CommandGroup(replacing: .appInfo) { Button { openAppSettingsWindow(tab: .about, updater: updater) } label: { Label("About \(Bundle.main.name)", systemImage: "info.circle.fill") } Divider() Button { openAppSettingsWindow(updater: updater) } label: { Label("Settings", systemImage: "gearshape") } .keyboardShortcut(",", modifiers: .command) Button { updater.checkForUpdates(sheet: true, force: true) } label: { Label("Check for Updates", systemImage: "tray.and.arrow.down.fill") } .keyboardShortcut("u", modifiers: .command) Button { showCustomAlert( title: "Warning!", message: "Pearcleaner and all of its files will be cleanly removed, are you sure?", okText: "Uninstall", style: .warning, onOk: { uninstallPearcleaner(appState: appState, locations: locations) } ) } label: { Label("Uninstall Pearcleaner", systemImage: "trash.fill") } } // Edit Menu CommandGroup(replacing: .undoRedo) { Button { let result = undoTrash() if result { if appState.currentPage == .plugins { // For plugins view, post notification to refresh NotificationCenter.default.post(name: NSNotification.Name("PluginsViewShouldRefresh"), object: nil) } else if appState.currentPage == .fileSearch { // For file search view, post notification to undo (has unique cache logic) NotificationCenter.default.post(name: NSNotification.Name("FileSearchViewShouldUndo"), object: nil) } else if appState.currentPage == .orphans { // For orphans view, post notification to refresh NotificationCenter.default.post(name: NSNotification.Name("ZombieViewShouldRefresh"), object: nil) } else if appState.currentPage == .packages { // For packages view, post notification to refresh NotificationCenter.default.post(name: NSNotification.Name("PackagesViewShouldRefresh"), object: nil) } else if appState.currentPage == .development { // For development view, post notification to refresh NotificationCenter.default.post(name: NSNotification.Name("DevelopmentViewShouldRefresh"), object: nil) } else { loadApps(folderPaths: fsm.folderPaths) // After reload, if we're viewing files, refresh the file view if appState.currentView == .files { Task { @MainActor in try? await Task.sleep(nanoseconds: 500_000_000) showAppInFiles(appInfo: appState.appInfo, appState: appState, locations: locations) } } } } } label: { Label("Undo Removal", systemImage: "clear") } .keyboardShortcut("z", modifiers: .command) .disabled(!FileManagerUndo.shared.undoManager.canUndo) Divider() Button { appState.showDeleteHistory = true } label: { Label("Delete History", systemImage: "clock.arrow.circlepath") } .keyboardShortcut("y", modifiers: [.command, .shift]) .disabled(UndoHistoryManager.shared.history.isEmpty) } // Window Menu CommandGroup(after: .sidebar) { Menu { Button { appState.currentPage = .applications } label: { Text("Applications") } .keyboardShortcut("1", modifiers: .command) Button { appState.currentPage = .development } label: { Text("Development") } .keyboardShortcut("2", modifiers: .command) Button { appState.currentPage = .fileSearch } label: { Text("File Search") } .keyboardShortcut("3", modifiers: .command) Button { appState.currentPage = .homebrew } label: { Text("Homebrew") } .keyboardShortcut("4", modifiers: .command) Button { appState.currentPage = .lipo } label: { Text("App Lipo") } .keyboardShortcut("5", modifiers: .command) Button { appState.currentPage = .orphans } label: { Text("Orphaned Files") } .keyboardShortcut("6", modifiers: .command) Button { appState.currentPage = .packages } label: { Text("Packages") } .keyboardShortcut("7", modifiers: .command) Button { appState.currentPage = .plugins } label: { Text("Plugins") } .keyboardShortcut("8", modifiers: .command) Button { appState.currentPage = .services } label: { Text("Services") } .keyboardShortcut("9", modifiers: .command) Button { appState.currentPage = .updater } label: { Text("Updater") } .keyboardShortcut("0", modifiers: .command) } label: { Label("Navigate To", systemImage: "location.north.fill") } } // Tools Menu CommandMenu(Text("Tools", comment: "Tools Menu")) { Button { Task { @MainActor in switch appState.currentPage { case .applications: if appState.currentView == .files { // User is viewing an app's files - refresh the files list let currentAppInfo = appState.appInfo updateOnMain { appState.selectedItems = [] } withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { showAppInFiles(appInfo: currentAppInfo, appState: appState, locations: locations) } } else { // User is on empty view or app list - refresh the app list withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { // Flush bundle caches before reloading to ensure fresh version info flushBundleCaches(for: appState.sortedApps) loadApps(folderPaths: fsm.folderPaths) } } case .development: NotificationCenter.default.post(name: NSNotification.Name("DevelopmentViewShouldRefresh"), object: nil) case .fileSearch: NotificationCenter.default.post(name: NSNotification.Name("FileSearchViewShouldRefresh"), object: nil) case .homebrew: NotificationCenter.default.post(name: NSNotification.Name("HomebrewViewShouldRefresh"), object: nil) case .lipo: NotificationCenter.default.post(name: NSNotification.Name("LipoViewShouldRefresh"), object: nil) case .orphans: NotificationCenter.default.post(name: NSNotification.Name("ZombieViewShouldRefresh"), object: nil) case .packages: NotificationCenter.default.post(name: NSNotification.Name("PackagesViewShouldRefresh"), object: nil) case .plugins: NotificationCenter.default.post(name: NSNotification.Name("PluginsViewShouldRefresh"), object: nil) case .services: NotificationCenter.default.post(name: NSNotification.Name("DaemonViewShouldRefresh"), object: nil) case .updater: NotificationCenter.default.post(name: NSNotification.Name("UpdaterViewShouldRefresh"), object: nil) } } } label: { Label("Refresh", systemImage: "arrow.counterclockwise.circle") } .keyboardShortcut("r", modifiers: .command) Button { if !appState.selectedItems.isEmpty { createTarArchive(appState: appState) } } label: { Label("Bundle Files...", systemImage: "archivebox") } .keyboardShortcut("b", modifiers: .command) .disabled(appState.selectedItems.isEmpty) Button { if !appState.appInfo.bundleIdentifier.isEmpty { saveURLsToFile(appState: appState) } } label: { Label("Export File Paths...", systemImage: "square.and.arrow.up") } .keyboardShortcut("e", modifiers: .command) .disabled(appState.selectedItems.isEmpty) Button { if !appState.appInfo.bundleIdentifier.isEmpty { saveURLsToFile(appState: appState, copy: true) } } label: { Label("Copy File Paths", systemImage: "square.and.arrow.up") } .keyboardShortcut("c", modifiers: [.command, .option]) .disabled(appState.selectedItems.isEmpty) } CommandGroup(after: .help) { // Debug options Button { withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { windowController.open(with: ConsoleView(), width: 600, height: 400) } } label: { Label("Debug Console", systemImage: "ladybug") } .keyboardShortcut("d", modifiers: .command) Button { // Export debug info to file exportDebugInfo(appState: appState) } label: { Label("Export Debug Info...", systemImage: "info.circle") } .keyboardShortcut("i", modifiers: [.command, .shift]) // Updater debug log export (only visible on Updater page) if appState.currentPage == .updater { Button { exportUpdaterDebugInfo() } label: { Label("Export Updater Debug Log...", systemImage: "arrow.triangle.2.circlepath.circle") } .keyboardShortcut("u", modifiers: [.command, .shift]) .disabled(!debugLogger.hasLogs) } Divider() // GitHub Menu Button { NSWorkspace.shared.open(URL(string: "https://github.com/alienator88/Pearcleaner")!) } label: { Label("View Repository", systemImage: "paperplane") } Button { NSWorkspace.shared.open(URL(string: "https://github.com/alienator88/Pearcleaner/releases")!) } label: { Label("View Releases", systemImage: "paperplane") } Button { NSWorkspace.shared.open(URL(string: "https://github.com/alienator88/Pearcleaner/issues")!) } label: { Label("View Issues", systemImage: "paperplane") } Divider() Button { NSWorkspace.shared.open(URL(string: "https://github.com/alienator88/Pearcleaner/issues/new/choose")!) } label: { Label("Submit New Issue", systemImage: "paperplane") } } } } ================================================ FILE: Pearcleaner/Logic/AppInfoFetch.swift ================================================ // // AppInfoFetch.swift // Pearcleaner // // Created by Alin Lupascu on 3/20/24. // import Foundation import SwiftUI import AlinFoundation // MARK: - Helper Functions /// Read Info.plist directly from disk without using Bundle cache /// This is useful when Bundle(url:) returns nil due to macOS not yet indexing a newly installed app private func readInfoPlistDirect(at appPath: URL) -> [String: Any]? { let infoPlistURL = appPath.appendingPathComponent("Contents/Info.plist") return NSDictionary(contentsOf: infoPlistURL) as? [String: Any] } // Metadata-based AppInfo Fetcher Class class MetadataAppInfoFetcher { static func getAppInfo(fromMetadata metadata: [String: Any], atPath path: URL) -> AppInfo? { // Extract metadata attributes for known fields var displayName = metadata["kMDItemDisplayName"] as? String ?? "" displayName = displayName.replacingOccurrences(of: ".app", with: "") let fsName = metadata["kMDItemFSName"] as? String ?? path.lastPathComponent let appName = displayName.isEmpty ? fsName : displayName let bundleIdentifier = metadata["kMDItemCFBundleIdentifier"] as? String ?? "" // Get version and build number directly from bundle Info.plist instead of metadata (always up-to-date) let (version, buildNumber): (String, String?) = { // Try Bundle(url:) first if let bundle = Bundle(url: path) { // Extract marketing version (CFBundleShortVersionString), fallback to CFBundleVersion if missing let shortVersion = bundle.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" let buildVer = bundle.infoDictionary?["CFBundleVersion"] as? String ?? "" // Use shortVersion if available, otherwise use buildVersion let marketingVersion = shortVersion.isEmpty ? buildVer : shortVersion // Build number is CFBundleVersion (only set if different from marketing version) let build = shortVersion.isEmpty ? nil : buildVer return (marketingVersion, build) } // Fallback: Read Info.plist directly from disk (useful for newly installed apps) if let infoDict = readInfoPlistDirect(at: path) { let shortVersion = infoDict["CFBundleShortVersionString"] as? String ?? "" let buildVer = infoDict["CFBundleVersion"] as? String ?? "" let marketingVersion = shortVersion.isEmpty ? buildVer : shortVersion let build = shortVersion.isEmpty ? nil : buildVer return (marketingVersion, build) } return ("", nil) }() // Size let logicalSize = metadata["kMDItemLogicalSize"] as? Int64 ?? 0 // Extract optional date fields early so we can pass them to fallback if needed let creationDate = metadata["kMDItemFSCreationDate"] as? Date let contentChangeDate = metadata["kMDItemFSContentChangeDate"] as? Date let lastUsedDate = metadata["kMDItemLastUsedDate"] as? Date let dateAdded = metadata["kMDItemDateAdded"] as? Date // Check if any of the critical fields are missing or invalid // Note: Size can be 0 for some apps where Spotlight hasn't indexed size yet, so only require core identifiers if appName.isEmpty || bundleIdentifier.isEmpty || version.isEmpty { // Fallback to the regular AppInfoFetcher for this app, but preserve dates we extracted from metadata return AppInfoFetcher.getAppInfo(atPath: path, dates: (creationDate, contentChangeDate, lastUsedDate, dateAdded)) } // Determine architecture type let arch = checkAppBundleArchitecture(at: path.path) // Use similar helper functions as `AppInfoFetcher` for attributes not found in metadata let wrapped = AppInfoFetcher.isDirectoryWrapped(path: path) let appIcon = AppInfoUtils.fetchAppIcon(for: path, wrapped: wrapped, md: true) let webApp = AppInfoUtils.isWebApp(appPath: path) let system = !path.path.contains(NSHomeDirectory()) // Get cask metadata (includes cask name and auto_updates flag) // Pass both display name, path, and bundle ID to handle various matching scenarios let caskInfo = getCaskInfo(for: appName, appPath: path, bundleId: bundleIdentifier) let cask = caskInfo?.caskName let autoUpdates = caskInfo?.autoUpdates // Get entitlements for the app let entitlements = getEntitlements(for: path.path) let teamIdentifier = getTeamIdentifier(for: path.path) // Detect update sources (done at load time for performance) let bundle = Bundle(url: path) let hasSparkle = AppCategoryDetector.checkForSparkle(bundle: bundle, infoDict: bundle?.infoDictionary) let isAppStore = AppCategoryDetector.checkForAppStore(bundle: bundle, path: path, wrapped: wrapped) // Extract App Store adamID from metadata (if available) let adamID: UInt64? = { if let adamValue = metadata["kMDItemAppStoreAdamID"] { // Handle NSNumber conversion if let number = adamValue as? NSNumber { return number.uint64Value } // Handle direct UInt64 if let uint = adamValue as? UInt64 { return uint } } return nil }() return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName, appVersion: version, appBuildNumber: buildNumber, appIcon: appIcon, webApp: webApp, wrapped: wrapped, system: system, arch: arch, cask: cask, steam: false, hasSparkle: hasSparkle, isAppStore: isAppStore, adamID: adamID, autoUpdates: autoUpdates, bundleSize: logicalSize, fileSize: [:], fileIcon: [:], creationDate: creationDate, contentChangeDate: contentChangeDate, lastUsedDate: lastUsedDate, dateAdded: dateAdded, entitlements: entitlements, teamIdentifier: teamIdentifier) } // MARK: - Phase 1: Fast Loading with AppInfoMini /// Fast lightweight app info loading for initial display /// Skips expensive operations: arch detection, entitlements, cask lookup, team identifier /// Always calculates bundleSize (required for sorting) static func getAppInfoMini(fromMetadata metadata: [String: Any], atPath path: URL) -> AppInfoMini? { // Extract basic info from metadata var displayName = metadata["kMDItemDisplayName"] as? String ?? "" displayName = displayName.replacingOccurrences(of: ".app", with: "") let fsName = metadata["kMDItemFSName"] as? String ?? path.lastPathComponent let appName = displayName.isEmpty ? fsName : displayName let bundleIdentifier = metadata["kMDItemCFBundleIdentifier"] as? String ?? "" // Get version from bundle Info.plist let version: String = { if let bundle = Bundle(url: path) { let shortVersion = bundle.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" let buildVer = bundle.infoDictionary?["CFBundleVersion"] as? String ?? "" return shortVersion.isEmpty ? buildVer : shortVersion } if let infoDict = readInfoPlistDirect(at: path) { let shortVersion = infoDict["CFBundleShortVersionString"] as? String ?? "" let buildVer = infoDict["CFBundleVersion"] as? String ?? "" return shortVersion.isEmpty ? buildVer : shortVersion } return "" }() // Require critical fields if appName.isEmpty || bundleIdentifier.isEmpty || version.isEmpty { return nil } // Get app icon (required for display, fast enough ~5-10ms) let wrapped = AppInfoFetcher.isDirectoryWrapped(path: path) let appIcon = AppInfoUtils.fetchAppIcon(for: path, wrapped: wrapped, md: true) // Determine if system app let system = !path.path.contains(NSHomeDirectory()) // Get bundleSize - ALWAYS calculate, never 0 (required for sorting) let bundleSize: Int64 = { // Try mdls metadata first (fast) if let mdlsSize = metadata["kMDItemLogicalSize"] as? Int64, mdlsSize > 0 { return mdlsSize } // Fallback: Calculate using totalSizeOnDisk (same as AppInfoFetcher) return totalSizeOnDisk(for: path) }() // Extract date fields from metadata let creationDate = metadata["kMDItemFSCreationDate"] as? Date let contentChangeDate = metadata["kMDItemFSContentChangeDate"] as? Date let lastUsedDate = metadata["kMDItemLastUsedDate"] as? Date let dateAdded = metadata["kMDItemDateAdded"] as? Date return AppInfoMini( id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName, appVersion: version, appIcon: appIcon, system: system, bundleSize: bundleSize, // ✅ Always calculated creationDate: creationDate, contentChangeDate: contentChangeDate, lastUsedDate: lastUsedDate, dateAdded: dateAdded ) } // MARK: - Phase 2: Upgrade to Full AppInfo /// Upgrade AppInfoMini to full AppInfo with all expensive properties /// Runs expensive operations: arch, entitlements, cask lookup, team identifier static func upgradeToFullAppInfo(mini: AppInfoMini) -> AppInfo { let path = mini.path // Get bundle for further inspection let bundle = Bundle(url: path) let infoDict = bundle?.infoDictionary ?? readInfoPlistDirect(at: path) // NOW do the expensive operations that were skipped in Phase 1 // Architecture detection (expensive: reads Mach-O binary headers) let arch = checkAppBundleArchitecture(at: path.path) // Entitlements scanning (expensive: code signing + recursive bundle scan) let entitlements = getEntitlements(for: path.path) // Team identifier (expensive: code signing read) let teamIdentifier = getTeamIdentifier(for: path.path) // Cask lookup (expensive: first-time builds entire lookup table) let caskInfo = getCaskInfo(for: mini.appName, appPath: path, bundleId: mini.bundleIdentifier) let cask = caskInfo?.caskName let autoUpdates = caskInfo?.autoUpdates // Detect app properties let wrapped = AppInfoFetcher.isDirectoryWrapped(path: path) let webApp = AppInfoUtils.isWebApp(appPath: path) // Detect update sources let hasSparkle = AppCategoryDetector.checkForSparkle(bundle: bundle, infoDict: infoDict) let isAppStore = AppCategoryDetector.checkForAppStore(bundle: bundle, path: path, wrapped: wrapped) // Get build number let appBuildNumber: String? = { if let dict = infoDict { let shortVersion = dict["CFBundleShortVersionString"] as? String ?? "" let buildVer = dict["CFBundleVersion"] as? String ?? "" return shortVersion.isEmpty ? nil : buildVer } return nil }() // Steam detection (not implemented in fast path, defaults to false) let steam = false // Adam ID (App Store ID) - would need to query mdls again, skip for now let adamID: UInt64? = nil return AppInfo( id: mini.id, path: mini.path, bundleIdentifier: mini.bundleIdentifier, appName: mini.appName, appVersion: mini.appVersion, appBuildNumber: appBuildNumber, appIcon: mini.appIcon, webApp: webApp, wrapped: wrapped, system: mini.system, arch: arch, // ✅ Phase 2 populated cask: cask, // ✅ Phase 2 populated steam: steam, hasSparkle: hasSparkle, // ✅ Phase 2 populated isAppStore: isAppStore, // ✅ Phase 2 populated adamID: adamID, autoUpdates: autoUpdates, bundleSize: mini.bundleSize, // Keep from Phase 1 lipoSavings: nil, fileSize: [:], // Populated when user selects app fileIcon: [:], // Populated when user selects app creationDate: mini.creationDate, contentChangeDate: mini.contentChangeDate, lastUsedDate: mini.lastUsedDate, dateAdded: mini.dateAdded, entitlements: entitlements, // ✅ Phase 2 populated teamIdentifier: teamIdentifier // ✅ Phase 2 populated ) } } // MARK: - Update Source Detection Helpers class AppCategoryDetector { /// Check if app has Sparkle update framework /// Detects Sparkle by checking for common Info.plist keys /// Excludes SetApp apps (they use Sparkle but are managed by SetApp) static func checkForSparkle(bundle: Bundle?, infoDict: [String: Any]?) -> Bool { guard let dict = infoDict ?? bundle?.infoDictionary else { return false } // Check for common Sparkle keys let hasSparkleKeys = dict["SUFeedURL"] != nil || dict["SUFeedUrl"] != nil || dict["SUPublicEDKey"] != nil || dict["SUPublicDSAKeyFile"] != nil || dict["SUEnableAutomaticChecks"] != nil // Exclude SetApp apps (they use Sparkle but are managed by SetApp) if hasSparkleKeys && isSetAppApp(bundle: bundle, infoDict: dict) { return false } return hasSparkleKeys } /// Check if app is a SetApp-managed app /// SetApp requires all apps to use the "-setapp" bundle ID suffix static func isSetAppApp(bundle: Bundle?, infoDict: [String: Any]?) -> Bool { // Try to get bundle ID from bundle first, then from infoDict if let bundleID = bundle?.bundleIdentifier ?? infoDict?["CFBundleIdentifier"] as? String { return bundleID.hasSuffix("-setapp") } return false } /// Check if app is from App Store /// Detects by checking for receipt or iTunes metadata static func checkForAppStore(bundle: Bundle?, path: URL, wrapped: Bool) -> Bool { // Check for wrapped iPad/iOS app first if wrapped { // Determine if path is wrapped or not // No wrapper: /Applications/App.app // With wrapper: /Applications/App.app/Wrapper/App.app let wrapperDir = path.appendingPathComponent("Wrapper") let isOuterWrapper = FileManager.default.fileExists(atPath: wrapperDir.path) // Use path directly if it's the outer wrapper, otherwise go up two levels to find it let outerWrapperPath = isOuterWrapper ? path : path.deletingLastPathComponent().deletingLastPathComponent() let iTunesMetadataPath = outerWrapperPath.appendingPathComponent("Wrapper/iTunesMetadata.plist").path if FileManager.default.fileExists(atPath: iTunesMetadataPath) { return true } } // Check for traditional Mac app receipt guard let receiptPath = bundle?.appStoreReceiptURL?.path else { return false } return FileManager.default.fileExists(atPath: receiptPath) } } class AppInfoUtils { /// Determines if the app is a web application by directly reading its `Info.plist` using the app path. static func isWebApp(appPath: URL) -> Bool { let infoPlistURL = appPath.appendingPathComponent("Contents/Info.plist") guard let infoDict = NSDictionary(contentsOf: infoPlistURL) as? [String: Any] else { return false } return (infoDict["LSTemplateApplication"] as? Bool ?? false) || (infoDict["CFBundleExecutable"] as? String == "app_mode_loader") } /// Determines if the app is a web application based on its bundle. static func isWebApp(bundle: Bundle?) -> Bool { guard let infoDict = bundle?.infoDictionary else { return false } return (infoDict["LSTemplateApplication"] as? Bool ?? false) || (infoDict["CFBundleExecutable"] as? String == "app_mode_loader") } /// Fetch app icon. static func fetchAppIcon(for path: URL, wrapped: Bool, md: Bool = false) -> NSImage? { let iconPath = wrapped ? (md ? path : path.deletingLastPathComponent().deletingLastPathComponent()) : path guard let appIcon = getIconForFileOrFolderNS(atPath: iconPath) else { printOS("App Icon not found for app at path: \(path)") return nil } let targetSize = NSSize(width: 50, height: 50) // OPTIMIZATION: Pre-render icon with cached representation using modern API // This must run on main thread to avoid deadlock with AppKit initialization func createRenderedIcon() -> NSImage { return NSImage(size: targetSize, flipped: false) { rect in appIcon.draw(in: rect) return true } } if Thread.isMainThread { return createRenderedIcon() } else { return DispatchQueue.main.sync { createRenderedIcon() } } } } func getMDLSMetadata(for paths: [String]) -> [String: [String: Any]]? { let kMDItemLogicalSize: CFString = "kMDItemLogicalSize" as CFString let kMDItemPhysicalSize: CFString = "kMDItemPhysicalSize" as CFString let kMDItemAppStoreAdamID: CFString = "kMDItemAppStoreAdamID" as CFString // List of metadata attributes to fetch let attributes: [CFString] = [ kMDItemFSCreationDate, kMDItemFSContentChangeDate, kMDItemLastUsedDate, kMDItemDateAdded, kMDItemDisplayName, kMDItemCFBundleIdentifier, kMDItemFSName, kMDItemLogicalSize, kMDItemPhysicalSize, kMDItemAppStoreAdamID ] // OPTIMIZATION: Process in parallel chunks let chunks = createOptimalChunks(from: paths, minChunkSize: 15, maxChunkSize: 50) let queue = DispatchQueue(label: "com.pearcleaner.metadata", qos: .userInitiated, attributes: .concurrent) let group = DispatchGroup() var allResults: [String: [String: Any]] = [:] let resultsQueue = DispatchQueue(label: "com.pearcleaner.metadata.results") for chunk in chunks { group.enter() queue.async { autoreleasepool { var chunkResults: [String: [String: Any]] = [:] // Process each path in this chunk for path in chunk { autoreleasepool { guard let mdItem = MDItemCreate(nil, path as CFString) else { return } var itemMetadata = [String: Any]() for attribute in attributes { if let value = MDItemCopyAttribute(mdItem, attribute) { itemMetadata[attribute as String] = value } } chunkResults[path] = itemMetadata } } // Safely merge results resultsQueue.sync { allResults.merge(chunkResults) { _, new in new } } } group.leave() } } group.wait() return allResults.isEmpty ? nil : allResults } // Add this helper extension if you don't have it yet: extension Array { func chunked(into size: Int) -> [[Element]] { return stride(from: 0, to: count, by: size).map { Array(self[$0.. AppInfo? { if isDirectoryWrapped(path: path) { return handleWrappedDirectory(atPath: path, dates: dates) } else { return createAppInfoFromBundle(atPath: path, wrapped: wrapped, dates: dates) } } public static func isDirectoryWrapped(path: URL) -> Bool { let wrapperURL = path.appendingPathComponent("Wrapper") return fileManager.fileExists(atPath: wrapperURL.path) } private static func handleWrappedDirectory(atPath path: URL, dates: (creation: Date?, contentChange: Date?, lastUsed: Date?, dateAdded: Date?)? = nil) -> AppInfo? { let wrapperURL = path.appendingPathComponent("Wrapper") do { let contents = try fileManager.contentsOfDirectory(at: wrapperURL, includingPropertiesForKeys: nil) guard let firstAppFile = contents.first(where: { $0.pathExtension == "app" }) else { printOS("No .app files found in the 'Wrapper' directory: \(wrapperURL)") return nil } let fullPath = wrapperURL.appendingPathComponent(firstAppFile.lastPathComponent) return getAppInfo(atPath: fullPath, wrapped: true, dates: dates) } catch { printOS("Error reading contents of 'Wrapper' directory: \(error.localizedDescription)\n\(wrapperURL)") return nil } } private static func createAppInfoFromBundle(atPath path: URL, wrapped: Bool, dates: (creation: Date?, contentChange: Date?, lastUsed: Date?, dateAdded: Date?)? = nil) -> AppInfo? { // Try Bundle(url:) first if let bundle = Bundle(url: path), let bundleIdentifier = bundle.bundleIdentifier { let appName = wrapped ? path.deletingLastPathComponent().deletingLastPathComponent().deletingPathExtension().lastPathComponent : path.localizedName() // Extract marketing version (CFBundleShortVersionString) - no fallback to build number let appVersion = (bundle.infoDictionary?["CFBundleShortVersionString"] as? String)?.isEmpty ?? true ? "" : bundle.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" // Extract build number (CFBundleVersion) separately let appBuildNumber = bundle.infoDictionary?["CFBundleVersion"] as? String let appIcon = AppInfoUtils.fetchAppIcon(for: path, wrapped: wrapped) let webApp = AppInfoUtils.isWebApp(bundle: bundle) let system = !path.path.contains(NSHomeDirectory()) // Get cask metadata (includes cask name and auto_updates flag) // Pass both display name, path, and bundle ID to handle various matching scenarios let caskInfo = getCaskInfo(for: appName, appPath: path, bundleId: bundleIdentifier) let cask = caskInfo?.caskName let autoUpdates = caskInfo?.autoUpdates let arch = checkAppBundleArchitecture(at: path.path) // Get entitlements for the app let entitlements = getEntitlements(for: path.path) let teamIdentifier = getTeamIdentifier(for: path.path) // Detect update sources (done at load time for performance) let hasSparkle = AppCategoryDetector.checkForSparkle(bundle: bundle, infoDict: bundle.infoDictionary) let isAppStore = AppCategoryDetector.checkForAppStore(bundle: bundle, path: path, wrapped: wrapped) // adamID not available in fallback path (no mdls metadata) let adamID: UInt64? = nil return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName, appVersion: appVersion, appBuildNumber: appBuildNumber, appIcon: appIcon, webApp: webApp, wrapped: wrapped, system: system, arch: arch, cask: cask, steam: false, hasSparkle: hasSparkle, isAppStore: isAppStore, adamID: adamID, autoUpdates: autoUpdates, bundleSize: 0, fileSize: [:], fileIcon: [:], creationDate: dates?.creation, contentChangeDate: dates?.contentChange, lastUsedDate: dates?.lastUsed, dateAdded: dates?.dateAdded, entitlements: entitlements, teamIdentifier: teamIdentifier) } // Fallback: Read Info.plist directly from disk (useful for newly installed apps where Bundle cache isn't ready) if let infoDict = readInfoPlistDirect(at: path), let bundleIdentifier = infoDict["CFBundleIdentifier"] as? String { let appName = wrapped ? path.deletingLastPathComponent().deletingLastPathComponent().deletingPathExtension().lastPathComponent : path.localizedName() // Extract marketing version (CFBundleShortVersionString) - no fallback to build number let appVersion = (infoDict["CFBundleShortVersionString"] as? String)?.isEmpty ?? true ? "" : infoDict["CFBundleShortVersionString"] as? String ?? "" // Extract build number (CFBundleVersion) separately let appBuildNumber = infoDict["CFBundleVersion"] as? String let appIcon = AppInfoUtils.fetchAppIcon(for: path, wrapped: wrapped) let webApp = AppInfoUtils.isWebApp(appPath: path) // Use path-based version since we don't have bundle let system = !path.path.contains(NSHomeDirectory()) // Get cask metadata (includes cask name and auto_updates flag) // Pass both display name, path, and bundle ID to handle various matching scenarios let caskInfo = getCaskInfo(for: appName, appPath: path, bundleId: bundleIdentifier) let cask = caskInfo?.caskName let autoUpdates = caskInfo?.autoUpdates let arch = checkAppBundleArchitecture(at: path.path) // Get entitlements for the app let entitlements = getEntitlements(for: path.path) let teamIdentifier = getTeamIdentifier(for: path.path) // Detect update sources (done at load time for performance) let hasSparkle = AppCategoryDetector.checkForSparkle(bundle: nil, infoDict: infoDict) let isAppStore = AppCategoryDetector.checkForAppStore(bundle: nil, path: path, wrapped: wrapped) // adamID not available in fallback path (no mdls metadata) let adamID: UInt64? = nil return AppInfo(id: UUID(), path: path, bundleIdentifier: bundleIdentifier, appName: appName, appVersion: appVersion, appBuildNumber: appBuildNumber, appIcon: appIcon, webApp: webApp, wrapped: wrapped, system: system, arch: arch, cask: cask, steam: false, hasSparkle: hasSparkle, isAppStore: isAppStore, adamID: adamID, autoUpdates: autoUpdates, bundleSize: 0, fileSize: [:], fileIcon: [:], creationDate: dates?.creation, contentChangeDate: dates?.contentChange, lastUsedDate: dates?.lastUsed, dateAdded: dates?.dateAdded, entitlements: entitlements, teamIdentifier: teamIdentifier) } // If both Bundle and direct reading failed, check if this might be a Steam game if let steamAppInfo = SteamAppInfoFetcher.checkForSteamGame(launcherPath: path) { return steamAppInfo } printOS("Bundle not found or missing bundle identifier at path: \(path)") return nil } } //MARK: Steam Games Support class SteamAppInfoFetcher { static let fileManager = FileManager.default /// Check if a failed app bundle is actually a Steam game launcher and find the real bundle static func checkForSteamGame(launcherPath: URL) -> AppInfo? { // Extract the app name from the launcher path (e.g., "Helltaker" from "Helltaker.app") let appName = launcherPath.deletingPathExtension().lastPathComponent // Check if this app exists in the Steam common directory let steamCommonPath = "\(NSHomeDirectory())/Library/Application Support/Steam/steamapps/common" let steamGamePath = steamCommonPath + "/" + appName guard fileManager.fileExists(atPath: steamGamePath) else { return nil } // Look for the actual .app bundle within the Steam game directory guard let actualAppBundle = findAppBundle(in: steamGamePath) else { return nil } // Create AppInfo using the actual Steam game bundle but keep the launcher path return createSteamAppInfo(launcherPath: launcherPath, actualBundlePath: actualAppBundle, gameFolderName: appName) } /// Find the .app bundle within a Steam game directory private static func findAppBundle(in directory: String) -> URL? { do { let contents = try fileManager.contentsOfDirectory(atPath: directory) // Look for .app files for item in contents { if item.hasSuffix(".app") { let appPath = directory + "/" + item let appURL = URL(fileURLWithPath: appPath) // Verify it has an Info.plist let infoPlistPath = appPath + "/Contents/Info.plist" if fileManager.fileExists(atPath: infoPlistPath) { return appURL } } } } catch { printOS("Error searching for app bundle in \(directory): \(error)") } return nil } /// Create AppInfo for Steam games using the launcher path but actual bundle info private static func createSteamAppInfo(launcherPath: URL, actualBundlePath: URL, gameFolderName: String) -> AppInfo? { guard let bundle = Bundle(url: actualBundlePath) else { printOS("Steam game bundle not found at path: \(actualBundlePath)") return nil } // Handle missing bundle identifier by providing a fallback let bundleIdentifier = bundle.bundleIdentifier?.isEmpty == false ? bundle.bundleIdentifier! : "com.no.bundleid" // Use the game folder name as the app name let appName = gameFolderName.capitalizingFirstLetter() // Extract marketing version (CFBundleShortVersionString) - no fallback to build number let appVersion = (bundle.infoDictionary?["CFBundleShortVersionString"] as? String)?.isEmpty ?? true ? "" : bundle.infoDictionary?["CFBundleShortVersionString"] as? String ?? "" // Extract build number (CFBundleVersion) separately let appBuildNumber = bundle.infoDictionary?["CFBundleVersion"] as? String // Use the actual bundle path for the icon (proper game icon) instead of launcher let appIcon = AppInfoUtils.fetchAppIcon(for: actualBundlePath, wrapped: false) let webApp = false let system = false // Steam games are never system apps let arch = checkAppBundleArchitecture(at: actualBundlePath.path) // Use the launcher path as the main path (so users see the ~/Applications version) // but store the actual bundle path info // Get entitlements for the Steam app let entitlements = getEntitlements(for: actualBundlePath.path) let teamIdentifier = getTeamIdentifier(for: actualBundlePath.path) // Steam games: typically no Sparkle or App Store (distributed via Steam) let hasSparkle = AppCategoryDetector.checkForSparkle(bundle: bundle, infoDict: bundle.infoDictionary) let isAppStore = false // Steam games are never from App Store let adamID: UInt64? = nil // Steam games don't have App Store adamID let autoUpdates: Bool? = nil // Steam games don't use Homebrew return AppInfo(id: UUID(), path: launcherPath, bundleIdentifier: bundleIdentifier, appName: appName, appVersion: appVersion, appBuildNumber: appBuildNumber, appIcon: appIcon, webApp: webApp, wrapped: false, system: system, arch: arch, cask: nil, steam: true, hasSparkle: hasSparkle, isAppStore: isAppStore, adamID: adamID, autoUpdates: autoUpdates, bundleSize: 0, fileSize: [:], fileIcon: [:], creationDate: nil, contentChangeDate: nil, lastUsedDate: nil, dateAdded: nil, entitlements: entitlements, teamIdentifier: teamIdentifier) } } private func getEntitlements(for appPath: String) -> [String]? { return autoreleasepool { () -> [String]? in let appURL = URL(fileURLWithPath: appPath) as CFURL var staticCode: SecStaticCode? // Create a static code object for the app guard SecStaticCodeCreateWithPath(appURL, [], &staticCode) == errSecSuccess, let code = staticCode else { return nil } // 1 << 2 is the bitmask for entitlements (kSecCSEntitlements) var info: CFDictionary? if SecCodeCopySigningInformation(code, SecCSFlags(rawValue: 1 << 2), &info) == errSecSuccess, let dict = info as? [String: Any], let entitlements = dict[kSecCodeInfoEntitlementsDict as String] as? [String: Any] { var results: [String] = [] // com.apple.security.application-groups if let appGroups = entitlements["com.apple.security.application-groups"] as? [String] { results.append(contentsOf: appGroups) } // com.apple.developer.icloud-container-identifiers if let icloudContainers = entitlements["com.apple.developer.icloud-container-identifiers"] as? [String] { results.append(contentsOf: icloudContainers) } // Note: Path-based entitlements (like temporary-exception.files paths) are not extracted // as they cause false positives by matching generic folder names like "Desktop", "Documents" // Skip generic binary names that could cause false positives let excludedNames = ["crashhandler", "crash handler", "electron"] // Scan Contents/MacOS folder for binary names // App binaries often leave behind files/folders matching their names let macosPath = URL(fileURLWithPath: appPath).appendingPathComponent("Contents/MacOS") if FileManager.default.fileExists(atPath: macosPath.path) { do { let files = try FileManager.default.contentsOfDirectory(atPath: macosPath.path) for file in files where !file.hasPrefix(".") { let fileNameLower = file.lowercased() // Add binary name if not already present, length >= 5, and not excluded if !results.contains(file) && file.count >= 5 && !excludedNames.contains(fileNameLower) { results.append(file) } } } catch { // Silently ignore errors (e.g., permission denied, folder doesn't exist) } } // Scan nested bundles one level deep in Contents/*/ for helper apps // Example: Contents/SharedSupport/wpscloudsvr.app let contentsPath = URL(fileURLWithPath: appPath).appendingPathComponent("Contents") if FileManager.default.fileExists(atPath: contentsPath.path) { do { let subdirs = try FileManager.default.contentsOfDirectory(at: contentsPath, includingPropertiesForKeys: nil) for subdir in subdirs where subdir.hasDirectoryPath { // Check for .app bundles in this subdirectory if let bundles = try? FileManager.default.contentsOfDirectory(at: subdir, includingPropertiesForKeys: nil) { for bundle in bundles where bundle.pathExtension == "app" { // Add bundle name (without .app extension) let bundleName = bundle.deletingPathExtension().lastPathComponent let bundleNameLower = bundleName.lowercased() if !results.contains(bundleName) && bundleName.count >= 5 && !excludedNames.contains(bundleNameLower) { results.append(bundleName) } // Scan nested bundle's MacOS folder for binary names let nestedMacOS = bundle.appendingPathComponent("Contents/MacOS") if let binaries = try? FileManager.default.contentsOfDirectory(atPath: nestedMacOS.path) { for binary in binaries where !binary.hasPrefix(".") && binary.count >= 5 { let binaryNameLower = binary.lowercased() if !results.contains(binary) && !excludedNames.contains(binaryNameLower) { results.append(binary) } } } } } } } catch { // Silently ignore errors (e.g., permission denied, folder doesn't exist) } } return results.isEmpty ? nil : results } return nil } } private func getTeamIdentifier(for appPath: String) -> String? { return autoreleasepool { let appURL = URL(fileURLWithPath: appPath) as CFURL var staticCode: SecStaticCode? // Create a static code object for the app guard SecStaticCodeCreateWithPath(appURL, [], &staticCode) == errSecSuccess, let code = staticCode else { return nil } // Get signing information with team identifier var info: CFDictionary? if SecCodeCopySigningInformation(code, SecCSFlags(rawValue: kSecCSSigningInformation), &info) == errSecSuccess, let dict = info as? [String: Any], let teamIdentifier = dict[kSecCodeInfoTeamIdentifier as String] as? String { return teamIdentifier } return nil } } ================================================ FILE: Pearcleaner/Logic/AppPathsFetch.swift ================================================ // // AppPathsFetch.swift // Pearcleaner // // Created by Alin Lupascu on 2/6/25. // import Foundation import AppKit import SwiftUI import AlinFoundation extension String { /// Strips trailing version numbers and digits from app names /// "Bartender 6" → "Bartender" /// "Firefox 120.0" → "Firefox" func strippingTrailingDigits() -> String { return self.replacingOccurrences( of: #"\s+\d+(\.\d+)*\s*$"#, with: "", options: .regularExpression ).trimmingCharacters(in: .whitespaces) } } class AppPathFinder { // Shared properties private var appInfo: AppInfo private var locations: Locations private var containerCollection: [URL] = [] private let collectionAccessQueue = DispatchQueue(label: "com.alienator88.Pearcleaner.appPathFinder.collectionAccess") @AppStorage("settings.general.searchSensitivity") private var sensitivityLevel: SearchSensitivityLevel = .strict @AppStorage("settings.general.searchTextContent") private var searchTextContent: Bool = false // Optional override sensitivity level for per-app settings private var overrideSensitivityLevel: SearchSensitivityLevel? // GUI-specific properties (can be nil for CLI) private var appState: AppState? private var undo: Bool = false private var completion: (() -> Void)? // Use a Set for fast membership testing private var collectionSet: Set = [] // Precompiled UUID regex private static let uuidRegex: NSRegularExpression = { return try! NSRegularExpression( pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$", options: .caseInsensitive ) }() // Change from lazy var to regular property initialized in init private let cachedIdentifiers: (formattedBundleId: String, bundleLastTwoComponents: String, formattedAppName: String, formattedAppNameStripped: String?, appNameLettersOnly: String, pathComponentName: String, useBundleIdentifier: Bool, formattedCompanyName: String?, formattedEntitlements: [String], formattedTeamIdentifier: String?, formattedBaseBundleId: String?) // Exclusion list for app file search (computed property to always get current list) private var formattedAppExclusionList: [String] { return FolderSettingsManager.shared.fileFolderPathsApps.map { $0.pearFormat() } } // Computed property to get the effective sensitivity level private var effectiveSensitivityLevel: SearchSensitivityLevel { return overrideSensitivityLevel ?? sensitivityLevel } // Initializer for both CLI and GUI init(appInfo: AppInfo, locations: Locations, appState: AppState? = nil, undo: Bool = false, sensitivityOverride: SearchSensitivityLevel? = nil, completion: (() -> Void)? = nil) { self.appInfo = appInfo self.locations = locations self.appState = appState self.undo = undo self.overrideSensitivityLevel = sensitivityOverride self.completion = completion // Initialize cachedIdentifiers eagerly and thread-safely let formattedBundleId = appInfo.bundleIdentifier.pearFormat() let bundleComponents = appInfo.bundleIdentifier .components(separatedBy: ".") .compactMap { $0 != "-" ? $0.lowercased() : nil } let bundleLastTwoComponents = bundleComponents.suffix(2).joined() let formattedAppName = appInfo.appName.pearFormat() let appNameLettersOnly = formattedAppName.filter { $0.isLetter } let pathComponentName = appInfo.path.lastPathComponent.replacingOccurrences(of: ".app", with: "") let useBundleIdentifier = AppPathFinder.isValidBundleIdentifier(appInfo.bundleIdentifier) // Extract company/dev name from 3-component bundle IDs (e.g., "com.knollsoft.Rectangle" -> "knollsoft") let formattedCompanyName: String? let rawComponents = appInfo.bundleIdentifier.components(separatedBy: ".") if rawComponents.count == 3 { formattedCompanyName = rawComponents[1].pearFormat() } else { formattedCompanyName = nil } // Pre-format entitlements once to avoid repeated formatting in the hot path let formattedEntitlements: [String] = appInfo.entitlements?.compactMap { entitlement in let formatted = entitlement.pearFormat() return formatted.isEmpty ? nil : formatted } ?? [] // Pre-format team identifier once let formattedTeamIdentifier = appInfo.teamIdentifier?.pearFormat() // Create base bundle ID by stripping common suffixes (for matching launch daemons/agents) // e.g., "com.objective-see.blockblock.helper" -> "com.objective-see.blockblock" let formattedBaseBundleId: String? let commonSuffixes = ["helper", "agent", "daemon", "service", "xpc", "launcher", "updater", "installer", "uninstaller", "login", "extension", "plugin"] if rawComponents.count >= 4 { let lastComponent = rawComponents.last?.lowercased() ?? "" if commonSuffixes.contains(lastComponent) { // Remove last component and format let baseBundleId = rawComponents.dropLast().joined(separator: ".") formattedBaseBundleId = baseBundleId.pearFormat() } else { formattedBaseBundleId = nil } } else { formattedBaseBundleId = nil } // Strip trailing digits from app name for Enhanced/Deep mode matching // "Bartender 6" → "bartender6" (regular) + "bartender" (stripped) let appNameStripped = appInfo.appName.strippingTrailingDigits() let formattedAppNameStripped: String? = { let stripped = appNameStripped.pearFormat() // Only use if different from regular formatted name and not empty return (stripped != formattedAppName && !stripped.isEmpty) ? stripped : nil }() self.cachedIdentifiers = (formattedBundleId, bundleLastTwoComponents, formattedAppName, formattedAppNameStripped, appNameLettersOnly, pathComponentName, useBundleIdentifier, formattedCompanyName, formattedEntitlements, formattedTeamIdentifier, formattedBaseBundleId) } // Process the initial URL private func initialURLProcessing() { if let url = URL(string: self.appInfo.path.absoluteString), !url.path.contains(".Trash") { let modifiedUrl = url.path.contains("Wrapper") ? url.deletingLastPathComponent().deletingLastPathComponent() : url collectionSet.insert(modifiedUrl) } } // Get all container URLs private func getAllContainers(bundleURL: URL) -> [URL] { var containers: [URL] = [] let bundleIdentifier = Bundle(url: bundleURL)?.bundleIdentifier guard let containerBundleIdentifier = bundleIdentifier else { return containers } if let groupContainer = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: containerBundleIdentifier) { if FileManager.default.fileExists(atPath: groupContainer.path) { containers.append(groupContainer) Task { @MainActor in GlobalConsoleManager.shared.appendOutput("Found group container: \(groupContainer.lastPathComponent)\n", source: CurrentPage.applications.title) } } } if let containersPath = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first?.appendingPathComponent("Containers") { do { let containerDirectories = try FileManager.default.contentsOfDirectory(at: containersPath, includingPropertiesForKeys: nil, options: .skipsHiddenFiles) for directory in containerDirectories { let directoryName = directory.lastPathComponent if AppPathFinder.uuidRegex.firstMatch(in: directoryName, options: [], range: NSRange(location: 0, length: directoryName.utf16.count)) != nil { let metadataPlistURL = directory.appendingPathComponent(".com.apple.containermanagerd.metadata.plist") if let metadataDict = NSDictionary(contentsOf: metadataPlistURL), let applicationBundleID = metadataDict["MCMMetadataIdentifier"] as? String { if applicationBundleID == self.appInfo.bundleIdentifier { containers.append(directory) Task { @MainActor in GlobalConsoleManager.shared.appendOutput("Found app container: \(directoryName)\n", source: CurrentPage.applications.title) } } } } } } catch { printOS("Error accessing Containers directory: \(error)") } } return containers } // Combined processing for directories and files private func processLocation(_ location: String) { processLocation(location, currentDepth: 0, maxDepth: 1, isLibraryRootSearch: false) } private func processLocation(_ location: String, currentDepth: Int, maxDepth: Int, isLibraryRootSearch: Bool = false) { if let contents = try? FileManager.default.contentsOfDirectory(atPath: location) { var localResults: [URL] = [] var subdirectoriesToSearch: [URL] = [] for scannedItem in contents { let scannedItemURL = URL(fileURLWithPath: location).appendingPathComponent(scannedItem) let normalizedItemName: String if scannedItemURL.hasDirectoryPath || scannedItemURL.pathExtension.isEmpty { // It's a directory or has no extension - don't remove anything normalizedItemName = scannedItem.pearFormat() } else { // It's a file with an extension - remove the extension normalizedItemName = (scannedItem as NSString).deletingPathExtension.pearFormat() } var isDirectory: ObjCBool = false if FileManager.default.fileExists(atPath: scannedItemURL.path, isDirectory: &isDirectory) { if shouldSkipItem(normalizedItemName, at: scannedItemURL) { continue } if specificCondition(normalizedItemName: normalizedItemName, scannedItemURL: scannedItemURL) { // Determine what to add: matched item or its parent (for vendor folders) let itemToAdd: URL // For depth=2 matches in Library root searches, check if parent is a vendor folder if isLibraryRootSearch && currentDepth == 2 { let parentURL = scannedItemURL.deletingLastPathComponent() let parentName = parentURL.lastPathComponent // Add parent directory if it's NOT a standard macOS directory // This captures vendor folders like /Library/Objective-See/ instead of /Library/Objective-See/LuLu/ if !Locations.standardLibrarySubdirectories.contains(parentName) { itemToAdd = parentURL } else { itemToAdd = scannedItemURL } } else { itemToAdd = scannedItemURL } localResults.append(itemToAdd) } // If this is a directory and we haven't reached max depth, mark for recursive search // Skip directories in the exclusion list when doing Library root deep searches if isDirectory.boolValue && currentDepth < maxDepth { // For Library root searches at depth 0, check if directory should be excluded from deep search if isLibraryRootSearch && currentDepth == 0 { let dirName = scannedItemURL.lastPathComponent if !skipDeepSearch.contains(dirName) { subdirectoriesToSearch.append(scannedItemURL) } } else { subdirectoriesToSearch.append(scannedItemURL) } } } } collectionAccessQueue.sync { collectionSet.formUnion(localResults) } // Recursively search subdirectories if we haven't reached max depth if currentDepth < maxDepth { for subdirectory in subdirectoriesToSearch { processLocation(subdirectory.path, currentDepth: currentDepth + 1, maxDepth: maxDepth, isLibraryRootSearch: isLibraryRootSearch) } } } } // Asynchronous collection for GUI usage private func collectLocations() { let dispatchGroup = DispatchGroup() for location in self.locations.apps.paths { dispatchGroup.enter() DispatchQueue.global(qos: .userInitiated).async { // Use depth=2 for Library directories to find files in vendor subdirectories // Example: /Library/Objective-See/LuLu, ~/Library/Microsoft/Edge let isLibRoot = self.isLibraryDirectory(location) let maxDepth = isLibRoot ? 2 : 1 self.processLocation(location, currentDepth: 0, maxDepth: maxDepth, isLibraryRootSearch: isLibRoot) dispatchGroup.leave() } } dispatchGroup.wait() } // Synchronous collection for CLI usage private func collectLocationsCLI() { for location in self.locations.apps.paths { // Use depth=2 for Library directories to find files in vendor subdirectories let isLibRoot = isLibraryDirectory(location) let maxDepth = isLibRoot ? 2 : 1 processLocation(location, currentDepth: 0, maxDepth: maxDepth, isLibraryRootSearch: isLibRoot) } } // Helper to determine if a location is a Library directory private func isLibraryDirectory(_ location: String) -> Bool { let home = FileManager.default.homeDirectoryForCurrentUser.path return location == "\(home)/Library" || location == "/Library" } // Skip items based on conditions and membership in collectionSet private func shouldSkipItem(_ normalizedItemName: String, at scannedItemURL: URL) -> Bool { var containsItem = false collectionAccessQueue.sync { containsItem = self.collectionSet.contains(scannedItemURL) } if containsItem { return true } for skipCondition in skipConditions { // Check path-based exclusions first for skipPath in skipCondition.skipPaths { if scannedItemURL.path.hasPrefix(skipPath) { return true } } // Check prefix-based exclusions if skipCondition.skipPrefix.contains(where: normalizedItemName.hasPrefix) { let isAllowed = skipCondition.allowPrefixes.contains(where: normalizedItemName.hasPrefix) if !isAllowed { return true } } } return false } // Check if an item meets specific conditions using cached identifiers private func specificCondition(normalizedItemName: String, scannedItemURL: URL) -> Bool { let cached = self.cachedIdentifiers // Special handling for Steam games: also check Desktop for shortcuts if scannedItemURL.path.contains("/Desktop/") && scannedItemURL.pathExtension == "app" { let desktopAppName = scannedItemURL.deletingPathExtension().lastPathComponent.pearFormat() if desktopAppName == cached.formattedAppName || desktopAppName == cached.appNameLettersOnly { return true } } // Special handling for Steam game main folder if self.appInfo.steam && scannedItemURL.path.contains("/Library/Application Support/Steam/steamapps/common/") { let folderName = scannedItemURL.lastPathComponent.pearFormat() // Check if this folder matches the game name if folderName == cached.formattedAppName || folderName == cached.appNameLettersOnly { return true } } // Special handling for Steam game manifest files if self.appInfo.steam && scannedItemURL.path.contains("/Library/Application Support/Steam/steamapps/") && scannedItemURL.lastPathComponent.hasPrefix("appmanifest_") && scannedItemURL.pathExtension == "acf" { // Extract the game ID from the filename (e.g., "appmanifest_1289310.acf" -> "1289310") let filename = scannedItemURL.lastPathComponent if let gameIdFromFile = extractGameId(from: filename) { // Get the game ID from the Steam launcher's run.sh file if let gameIdFromLauncher = getSteamGameId(from: self.appInfo.path) { return gameIdFromFile == gameIdFromLauncher } } } // Check entitlements-based matching (using pre-formatted entitlements) // Strict: exact match only, Enhanced/Deep: contains match for entitlementFormatted in cached.formattedEntitlements { let isMatch = effectiveSensitivityLevel == .strict ? normalizedItemName == entitlementFormatted : normalizedItemName.contains(entitlementFormatted) if isMatch { return true } } for condition in conditions { if cached.useBundleIdentifier && cached.formattedBundleId.contains(condition.bundle_id) { if condition.exclude.contains(where: { normalizedItemName.contains($0) }) { return false } if condition.include.contains(where: { normalizedItemName.contains($0) }) { return true } } } if self.appInfo.webApp { return normalizedItemName.contains(cached.formattedBundleId) } let fullBundleMatch = normalizedItemName.contains(cached.formattedBundleId) let sensitivity = effectiveSensitivityLevel == .strict // Prevent false matches when cached values are empty strings let appNameMatch = !cached.formattedAppName.isEmpty && (sensitivity ? normalizedItemName == cached.formattedAppName : normalizedItemName.contains(cached.formattedAppName)) let pathNameMatch = !cached.pathComponentName.isEmpty && (sensitivity ? normalizedItemName == cached.pathComponentName : normalizedItemName.contains(cached.pathComponentName)) let appNameLettersMatch = !cached.appNameLettersOnly.isEmpty && (sensitivity ? normalizedItemName == cached.appNameLettersOnly : normalizedItemName.contains(cached.appNameLettersOnly)) // Bundle ID component matching (Enhanced/Deep levels only) let twoComponentMatch: Bool if effectiveSensitivityLevel != .strict { twoComponentMatch = normalizedItemName.contains(cached.bundleLastTwoComponents) } else { twoComponentMatch = false } // Company name matching (Deep level only) let companyMatch: Bool if effectiveSensitivityLevel == .deep, let company = cached.formattedCompanyName, !company.isEmpty { companyMatch = normalizedItemName.contains(company) } else { companyMatch = false } // Team identifier matching (Deep level only) let teamIdMatch: Bool if effectiveSensitivityLevel == .deep, let teamId = cached.formattedTeamIdentifier, !teamId.isEmpty { teamIdMatch = normalizedItemName.contains(teamId) } else { teamIdMatch = false } // Base bundle ID matching (for apps with .helper/.agent/etc. suffixes) // Matches launch daemons/agents that use base bundle ID without suffix let baseBundleIdMatch: Bool if let baseBundleId = cached.formattedBaseBundleId, !baseBundleId.isEmpty { baseBundleIdMatch = normalizedItemName.contains(baseBundleId) } else { baseBundleIdMatch = false } // Stripped app name matching (Enhanced/Deep only) // Matches files with version-stripped names: "Bartender 6" → also matches "bartender" files let strippedAppNameMatch: Bool if effectiveSensitivityLevel != .strict, let stripped = cached.formattedAppNameStripped, !stripped.isEmpty { strippedAppNameMatch = normalizedItemName.contains(stripped) } else { strippedAppNameMatch = false } return (cached.useBundleIdentifier && fullBundleMatch) || (appNameMatch || pathNameMatch || appNameLettersMatch) || twoComponentMatch || companyMatch || teamIdMatch || baseBundleIdMatch || strippedAppNameMatch } // Helper function to extract game ID from manifest filename private func extractGameId(from filename: String) -> String? { // Extract from "appmanifest_1289310.acf" -> "1289310" let components = filename.components(separatedBy: "_") if components.count >= 2 { let gameIdWithExtension = components[1] return gameIdWithExtension.components(separatedBy: ".").first } return nil } // Helper function to get Steam game ID from the launcher's run.sh file private func getSteamGameId(from appPath: URL) -> String? { let runShPath = appPath.appendingPathComponent("Contents/MacOS/run.sh") guard FileManager.default.fileExists(atPath: runShPath.path) else { return nil } do { let content = try String(contentsOf: runShPath, encoding: .utf8) // Look for "steam://run/" pattern and extract the number after it if let range = content.range(of: "steam://run/") { let afterRun = String(content[range.upperBound...]) // Extract the number (game ID) which should be at the beginning let gameId = afterRun.components(separatedBy: CharacterSet.decimalDigits.inverted).first return gameId?.isEmpty == false ? gameId : nil } } catch { printOS("Error reading run.sh file: \(error)") } return nil } // Check for associated zombie files private func fetchAssociatedZombieFiles() -> [URL] { let storedFiles = ZombieFileStorage.shared.getAssociatedFiles(for: self.appInfo.path) // Only return files that actually exist on disk // Keep associations in storage so they can be restored if file comes back return storedFiles.filter { FileManager.default.fileExists(atPath: $0.path) } } // Helper method to check bundle identifier validity - now static private static func isValidBundleIdentifier(_ bundleIdentifier: String) -> Bool { let components = bundleIdentifier.components(separatedBy: ".") if components.count == 1 { return bundleIdentifier.count >= 5 } return true } // Check spotlight index for leftovers missed by manual search private func spotlightSupplementalPaths() -> [URL] { // Spotlight enabled for all levels with sensitivity-appropriate matching // Strict: Exact matches only (via ==[cd] predicate and post-filter) // Enhanced/Deep: Contains matches (via CONTAINS[cd] predicate) updateOnMain { self.appState?.progressStep = 1 } var results: [URL] = [] let query = NSMetadataQuery() let appName = self.appInfo.appName let bundleID = self.appInfo.bundleIdentifier // Build predicate based on sensitivity level switch self.effectiveSensitivityLevel { case .strict: // Strict: Only exact filename matches query.predicate = NSPredicate(format: "kMDItemDisplayName ==[cd] %@ OR kMDItemDisplayName ==[cd] %@", appName, bundleID) case .enhanced: // Enhanced: Partial matching in name and path query.predicate = NSPredicate(format: "kMDItemDisplayName CONTAINS[cd] %@ OR kMDItemPath CONTAINS[cd] %@", appName, bundleID) case .deep: // Deep: Fuzzy search with metadata and AND logic for multi-word names var subpredicates: [NSPredicate] = [ // DisplayName: appName OR bundleID NSCompoundPredicate(orPredicateWithSubpredicates: [ NSPredicate(format: "kMDItemDisplayName CONTAINS[cd] %@", appName), NSPredicate(format: "kMDItemDisplayName CONTAINS[cd] %@", bundleID) ]), // Path: appName OR bundleID NSCompoundPredicate(orPredicateWithSubpredicates: [ NSPredicate(format: "kMDItemPath CONTAINS[cd] %@", appName), NSPredicate(format: "kMDItemPath CONTAINS[cd] %@", bundleID) ]), // Comment: appName OR bundleID NSCompoundPredicate(orPredicateWithSubpredicates: [ NSPredicate(format: "kMDItemComment CONTAINS[cd] %@", appName), NSPredicate(format: "kMDItemComment CONTAINS[cd] %@", bundleID) ]), // Creator: appName OR bundleID NSCompoundPredicate(orPredicateWithSubpredicates: [ NSPredicate(format: "kMDItemCreator ==[cd] %@", appName), NSPredicate(format: "kMDItemCreator ==[cd] %@", bundleID) ]), // Copyright: appName OR bundleID (often contains developer/company info) NSCompoundPredicate(orPredicateWithSubpredicates: [ NSPredicate(format: "kMDItemCopyright CONTAINS[cd] %@", appName), NSPredicate(format: "kMDItemCopyright CONTAINS[cd] %@", bundleID) ]), // EncodingApplications: appName only (array of apps that processed the file) NSPredicate(format: "kMDItemEncodingApplications CONTAINS[cd] %@", appName) ] // TextContent: appName OR bundleID (optional, controlled by user setting) if searchTextContent { subpredicates.append( NSCompoundPredicate(orPredicateWithSubpredicates: [ NSPredicate(format: "kMDItemTextContent CONTAINS[cd] %@", appName), NSPredicate(format: "kMDItemTextContent CONTAINS[cd] %@", bundleID) ]) ) } // Add wildcard predicate: ALL name parts must be present (AND logic) let nameParts = appName.split(separator: " ") if nameParts.count > 1 { // For multi-word names: each part must appear in display name OR path let partPredicates = nameParts.map { part in NSCompoundPredicate(orPredicateWithSubpredicates: [ NSPredicate(format: "kMDItemDisplayName LIKE[cd] %@", "*\(part)*"), NSPredicate(format: "kMDItemPath LIKE[cd] %@", "*\(part)*") ]) } // All parts must be present (AND) let allPartsPresent = NSCompoundPredicate(andPredicateWithSubpredicates: partPredicates) subpredicates.append(allPartsPresent) } else { // Single word: just add LIKE for that word subpredicates.append(NSPredicate(format: "kMDItemDisplayName LIKE[cd] %@", "*\(appName)*")) } // Combine all conditions with OR (any match wins) query.predicate = NSCompoundPredicate(orPredicateWithSubpredicates: subpredicates) } query.searchScopes = [NSMetadataQueryUserHomeScope] let currentRunLoop = CFRunLoopGetCurrent() let finishedNotification = NotificationCenter.default.addObserver(forName: NSNotification.Name.NSMetadataQueryDidFinishGathering, object: query, queue: nil) { _ in query.disableUpdates() query.stop() results = query.results.compactMap { ($0 as? NSMetadataItem)?.value(forAttribute: kMDItemPath as String) }.compactMap { URL(fileURLWithPath: $0 as! String) } // Post-filter: Only for Strict level if self.effectiveSensitivityLevel == .strict { let nameFormatted = appName.pearFormat() let bundleFormatted = bundleID.pearFormat() results = results.filter { url in let pathFormatted = url.lastPathComponent.pearFormat() return pathFormatted == nameFormatted || pathFormatted == bundleFormatted } } CFRunLoopStop(currentRunLoop) } query.start() // Timeout after 5 seconds DispatchQueue.global().asyncAfter(deadline: .now() + 5) { CFRunLoopStop(currentRunLoop) } CFRunLoopRun() // Limit results to prevent excessive post-processing delays if results.count > 500 { results = Array(results.prefix(500)) } NotificationCenter.default.removeObserver(finishedNotification) return results } // Finalize the collection for GUI usage private func finalizeCollection() { DispatchQueue.global(qos: .userInitiated).async { let outliers = self.handleOutliers() let outliersEx = self.handleOutliers(include: false) var tempCollection: [URL] = [] self.collectionAccessQueue.sync { tempCollection = Array(self.collectionSet) } Task { @MainActor in GlobalConsoleManager.shared.appendOutput("Found \(tempCollection.count) files from manual search\n", source: CurrentPage.applications.title) } tempCollection.append(contentsOf: self.containerCollection) tempCollection.append(contentsOf: outliers) // Insert spotlight results before sorting and filtering Task { @MainActor in GlobalConsoleManager.shared.appendOutput("Running Spotlight supplemental search...\n", source: CurrentPage.applications.title) } let spotlightResults = self.spotlightSupplementalPaths() let spotlightOnly = spotlightResults.filter { !self.collectionSet.contains($0) } if spotlightOnly.count > 0 { Task { @MainActor in GlobalConsoleManager.shared.appendOutput("Spotlight found \(spotlightOnly.count) additional files\n", source: CurrentPage.applications.title) } } tempCollection.append(contentsOf: spotlightOnly) let excludePaths = outliersEx.map { $0.path } tempCollection.removeAll { url in excludePaths.contains(url.path) } // Apply app exclusion filter to ALL discovered files (from all code paths) tempCollection.removeAll { fileURL in let normalizedPath = fileURL.standardizedFileURL.path.pearFormat() return self.formattedAppExclusionList.contains(normalizedPath) || self.formattedAppExclusionList.contains(where: { normalizedPath.contains($0) }) } let sortedCollection = tempCollection.map { $0.standardizedFileURL }.sorted(by: { $0.path < $1.path }) var filteredCollection: [URL] = [] for url in sortedCollection { // Remove any existing child paths of the current URL filteredCollection.removeAll { $0.path.hasPrefix(url.path + "/") } // Only add if it's not already a subpath of an existing item if !filteredCollection.contains(where: { url.path.hasPrefix($0.path + "/") }) { filteredCollection.append(url) } } Task { @MainActor in GlobalConsoleManager.shared.appendOutput("Calculating file sizes for \(filteredCollection.count) items...\n", source: CurrentPage.applications.title) } self.handlePostProcessing(sortedCollection: filteredCollection) } } // Finalize the collection for CLI usage private func finalizeCollectionCLI() -> Set { let outliers = handleOutliers() let outliersEx = handleOutliers(include: false) var tempCollection: [URL] = [] self.collectionAccessQueue.sync { tempCollection = Array(self.collectionSet) } tempCollection.append(contentsOf: self.containerCollection) tempCollection.append(contentsOf: outliers) // Insert spotlight results before sorting and filtering let spotlightResults = self.spotlightSupplementalPaths() let spotlightOnly = spotlightResults.filter { !self.collectionSet.contains($0) } // printOS("Spotlight index found: \(spotlightOnly.count)") tempCollection.append(contentsOf: spotlightOnly) let excludePaths = outliersEx.map { $0.path } tempCollection.removeAll { url in excludePaths.contains(url.path) } // Apply app exclusion filter to ALL discovered files (from all code paths) tempCollection.removeAll { fileURL in let normalizedPath = fileURL.standardizedFileURL.path.pearFormat() return formattedAppExclusionList.contains(normalizedPath) || formattedAppExclusionList.contains(where: { normalizedPath.contains($0) }) } let sortedCollection = tempCollection.map { $0.standardizedFileURL }.sorted(by: { $0.path < $1.path }) var filteredCollection: [URL] = [] var previousUrl: URL? for url in sortedCollection { if let previousUrl = previousUrl, url.path.hasPrefix(previousUrl.path + "/") { continue } filteredCollection.append(url) previousUrl = url } if filteredCollection.count == 1, let firstURL = filteredCollection.first, firstURL.path.contains(".Trash") { filteredCollection.removeAll() } return Set(filteredCollection) } // Handle outlier paths based on conditions private func handleOutliers(include: Bool = true) -> [URL] { var outliers: [URL] = [] let bundleIdentifier = self.appInfo.bundleIdentifier.pearFormat() let matchingConditions = conditions.filter { condition in bundleIdentifier.contains(condition.bundle_id) } for condition in matchingConditions { if include { if let forceIncludes = condition.includeForce { for path in forceIncludes { outliers.append(path) } } } else { if let excludeForce = condition.excludeForce { for path in excludeForce { outliers.append(path) } } } } return outliers } // Post-processing: calculate file details, update state, and call completion private func handlePostProcessing(sortedCollection: [URL]) { // Fetch associated zombie files and add them to the collection var tempCollection = sortedCollection let associatedFiles = fetchAssociatedZombieFiles() for file in associatedFiles { if !tempCollection.contains(file) { tempCollection.append(file) // Now it's properly included } } var fileSize: [URL: Int64] = [:] var fileIcon: [URL: NSImage?] = [:] let chunks = createOptimalChunks(from: tempCollection) let queue = DispatchQueue(label: "size-calculation", qos: .userInitiated, attributes: .concurrent) let group = DispatchGroup() for chunk in chunks { group.enter() queue.async { var localFileSize: [URL: Int64] = [:] var localFileIcon: [URL: NSImage?] = [:] for path in chunk { let size = spotlightSizeForURL(path) localFileSize[path] = size localFileIcon[path] = self.getSmartIcon(for: path) } // Merge results safely DispatchQueue.main.sync { fileSize.merge(localFileSize) { $1 } fileIcon.merge(localFileIcon) { $1 } } group.leave() } } group.wait() let arch = checkAppBundleArchitecture(at: self.appInfo.path.path) var updatedCollection = tempCollection if updatedCollection.count == 1, let firstURL = updatedCollection.first, firstURL.path.contains(".Trash") { updatedCollection.removeAll() } DispatchQueue.main.async { self.appInfo.fileSize = fileSize self.appInfo.fileIcon = fileIcon self.appInfo.arch = arch self.appState?.appInfo = self.appInfo if !self.undo { self.appState?.selectedItems = Set(updatedCollection) } self.appState?.progressStep = 0 self.appState?.showProgress = false let totalSize = fileSize.values.reduce(0, +) let sizeString = ByteCountFormatter.string(fromByteCount: totalSize, countStyle: .file) GlobalConsoleManager.shared.appendOutput("✓ Found \(updatedCollection.count) items (\(sizeString))\n", source: CurrentPage.applications.title) self.completion?() } } // Public method for GUI func findPaths() { Task(priority: .background) { await GlobalConsoleManager.shared.appendOutput("Searching for files related to \(self.appInfo.appName)...\n", source: CurrentPage.applications.title) if self.appInfo.webApp { await GlobalConsoleManager.shared.appendOutput("Detected web app, scanning containers...\n", source: CurrentPage.applications.title) self.containerCollection = self.getAllContainers(bundleURL: self.appInfo.path) self.initialURLProcessing() self.finalizeCollection() } else { await GlobalConsoleManager.shared.appendOutput("Scanning \(self.locations.apps.paths.count) system locations...\n", source: CurrentPage.applications.title) self.containerCollection = self.getAllContainers(bundleURL: self.appInfo.path) self.initialURLProcessing() self.collectLocations() self.finalizeCollection() } } } // Public method for CLI func findPathsCLI() -> Set { if self.appInfo.webApp { self.containerCollection = self.getAllContainers(bundleURL: self.appInfo.path) self.initialURLProcessing() return finalizeCollectionCLI() } else { self.containerCollection = self.getAllContainers(bundleURL: self.appInfo.path) self.initialURLProcessing() self.collectLocationsCLI() return finalizeCollectionCLI() } } // Custom icon function that handles .app folders intelligently private func getSmartIcon(for path: URL) -> NSImage? { // For wrapped apps, check if this is the container path if self.appInfo.wrapped { // Get container path from inner app path let containerPath = self.appInfo.path .deletingLastPathComponent() // Remove ActualApp.app .deletingLastPathComponent() // Remove Wrapper -> get Container.app // If the current path matches the container, use the app's icon if path.absoluteString == containerPath.absoluteString { return self.appInfo.appIcon } } // Regular logic for all other files if path.pathExtension == "app" { let contentsPath = path.appendingPathComponent("Contents") var isDirectory: ObjCBool = false if FileManager.default.fileExists(atPath: contentsPath.path, isDirectory: &isDirectory) && isDirectory.boolValue { // It's a real app bundle, get the app icon return getIconForFileOrFolderNS(atPath: path) } else { // It's just a folder that ends with .app, get the folder icon return NSWorkspace.shared.icon(for: .folder) } } else { // For all other files/folders, use the standard function return getIconForFileOrFolderNS(atPath: path) } } } // Get size using Spotlight metadata, fallback to manual calculation if needed private func spotlightSizeForURL(_ url: URL) -> Int64 { // Check if this is a directory (not a bundle like .app) var isDirectory: ObjCBool = false FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory) // For plain directories, Spotlight metadata is unreliable (reports wrong size) // Skip to manual calculation for directories (Containers, regular folders, etc.) if isDirectory.boolValue { return totalSizeOnDisk(for: url) } // For files and bundles (.app, .framework), use Spotlight metadata let metadataItem = NSMetadataItem(url: url) if let logical = metadataItem?.value(forAttribute: "kMDItemLogicalSize") as? Int64 { return logical } // Fallback to manual calculation if Spotlight has no data return totalSizeOnDisk(for: url) } ================================================ FILE: Pearcleaner/Logic/AppState.swift ================================================ // // AppState.swift // Pearcleaner // // Created by Alin Lupascu on 10/31/23. // import AlinFoundation import FinderSync import Foundation import SwiftUI let home = FileManager.default.homeDirectoryForCurrentUser.path struct AutoSlimStats: Codable { var originalSize: Int64 var currentSize: Int64 var lastRunVersion: String } class AppState: ObservableObject { // MARK: - Singleton Instance static let shared = AppState() @Published var appInfo: AppInfo @Published var zombieFile: ZombieFile @Published var sortedApps: [AppInfo] = [] @Published var selectedItems = Set() @Published var currentView = CurrentDetailsView.empty @Published var currentPage: CurrentPage // Initialized from stored preference in init() @Published var showAlert: Bool = false @Published var showDeleteHistory: Bool = false @Published var sidebar: Bool = true @Published var showProgress: Bool = false @Published var isBrewCleanupInProgress: Bool = false @Published var progressStep: Int = 0 @Published var leftoverProgress: (String, Double) = ("", 0.0) @Published var finderExtensionEnabled: Bool = false @Published var externalMode: Bool = false @Published var multiMode: Bool = false @Published var externalPaths: [URL] = [] // for handling multiple app from drops or deeplinks @Published var selectedEnvironment: PathEnv? // for handling dev environments @Published var trashError: Bool = false @Published var isGridMode: Bool = false // Volume information @Published var volumeInfos: [VolumeInfo] = [] @Published var volumeAnimationShown: Bool = false // Per-app sensitivity level (session-only, not persisted) @Published var perAppSensitivity: [String: SearchSensitivityLevel] = [:] func getBundleSize(for appInfo: AppInfo, updateState: @escaping (Int64) -> Void) { DispatchQueue.global(qos: .userInitiated).async { // Step 1: Check if the size is available and not 0 in the sortedApps cache if let existingAppInfo = self.sortedApps.first(where: { $0.path == appInfo.path }) { if existingAppInfo.bundleSize > 0 { // Cached size is available, update the state immediately DispatchQueue.main.async { updateState(existingAppInfo.bundleSize) } return } } // Step 2: If we reach here, we need to calculate the size let calculatedSize = totalSizeOnDisk(for: appInfo.path) DispatchQueue.main.async { // Update the state and the array updateState(calculatedSize) if let index = self.sortedApps.firstIndex(where: { $0.path == appInfo.path }) { var updatedAppInfo = self.sortedApps[index] updatedAppInfo.bundleSize = calculatedSize updatedAppInfo.arch = isOSArm() ? .arm : .intel self.sortedApps[index] = updatedAppInfo } } } } init() { // Initialize currentPage from stored startup view preference let storedStartupView = UserDefaults.standard.integer(forKey: "settings.interface.startupView") // Load hidden pages let hiddenPages = AppState.loadHiddenPages() // Validate: If startup page is hidden, default to .applications if hiddenPages.contains(storedStartupView) { self.currentPage = .applications UserDefaults.standard.set(CurrentPage.applications.rawValue, forKey: "settings.interface.startupView") } else { self.currentPage = CurrentPage(rawValue: storedStartupView) ?? .applications } self.appInfo = AppInfo( id: UUID(), path: URL(fileURLWithPath: ""), bundleIdentifier: "", appName: "", appVersion: "", appBuildNumber: nil, appIcon: nil, webApp: false, wrapped: false, system: false, arch: .empty, cask: nil, steam: false, hasSparkle: false, isAppStore: false, adamID: nil, autoUpdates: nil, bundleSize: 0, fileSize: [:], fileIcon: [:], creationDate: nil, contentChangeDate: nil, lastUsedDate: nil, dateAdded: nil, entitlements: nil, teamIdentifier: nil ) self.zombieFile = ZombieFile( id: UUID(), fileSize: [:], fileIcon: [:] ) updateExtensionStatus() NotificationCenter.default.addObserver( self, selector: #selector(updateExtensionStatus), name: NSApplication.didBecomeActiveNotification, object: nil ) } @objc func updateExtensionStatus() { let extensionStatus = FIFinderSyncController.isExtensionEnabled DispatchQueue.main.async { self.finderExtensionEnabled = extensionStatus } } // MARK: - Hidden Pages Management static func loadHiddenPages() -> Set { guard let data = UserDefaults.standard.data(forKey: "settings.interface.hiddenPages"), let decoded = try? JSONDecoder().decode(Set.self, from: data) else { return [] } return decoded } static func saveHiddenPages(_ pages: Set) { if let encoded = try? JSONEncoder().encode(pages) { UserDefaults.standard.set(encoded, forKey: "settings.interface.hiddenPages") } } // Add this method to restore zombie file associations func restoreZombieAssociations() { let zombieStorage = ZombieFileStorage.shared // Clean up invalid associations first let validAppPaths = sortedApps.map { $0.path } zombieStorage.cleanupInvalidAssociations(validAppPaths: validAppPaths) // For each app that has associations, add the zombie file URLs to their fileSize dictionary // The actual sizes will be calculated later during the scan for appInfo in sortedApps { let associatedFiles = zombieStorage.getAssociatedFiles(for: appInfo.path) // Add zombie files to this app's file tracking // We'll add them with size 0 - the real sizes will be calculated during scan for zombieFile in associatedFiles { // Only add if the file still exists if FileManager.default.fileExists(atPath: zombieFile.path) { // Add to the current appInfo if it matches, or find and update the correct one if let appIndex = sortedApps.firstIndex(where: { $0.path == appInfo.path }) { sortedApps[appIndex].fileSize[zombieFile] = 0 // Placeholder size // Icon will be fetched during normal scan process } } } } } func loadVolumeInfo() { DispatchQueue.global(qos: .userInitiated).async { var volumes: [VolumeInfo] = [] // First, add root volume (/) if let rootVolume = self.getVolumeInfo(for: URL(fileURLWithPath: "/")) { volumes.append(rootVolume) #if DEBUG // Duplicate for testing let duplicateRoot = VolumeInfo( name: "\(rootVolume.name) Debug", path: rootVolume.path, icon: rootVolume.icon, totalSpace: rootVolume.totalSpace, usedSpace: rootVolume.usedSpace, realAvailableSpace: rootVolume.realAvailableSpace, purgeableSpace: rootVolume.purgeableSpace, isExternal: false ) volumes.append(duplicateRoot) #endif } // Then enumerate all mounted volumes in /Volumes let volumesPath = "/Volumes" if let volumeContents = try? FileManager.default.contentsOfDirectory( atPath: volumesPath) { for volumeName in volumeContents { // Skip dot folders (hidden folders like .timemachine) if volumeName.hasPrefix(".") { continue } let volumePath = "\(volumesPath)/\(volumeName)" let volumeURL = URL(fileURLWithPath: volumePath) // Resolve symlinks let resolvedURL = volumeURL.resolvingSymlinksInPath() // Skip if it's the same as root (to avoid duplicates) if resolvedURL.path == "/" { continue } // Skip Time Machine volumes if !self.isTimeMachineVolume(url: resolvedURL) { if let volumeInfo = self.getVolumeInfo( for: resolvedURL, displayName: volumeName) { volumes.append(volumeInfo) } } } } DispatchQueue.main.async { // Preserve hasAnimated state from existing volumes for i in 0.. VolumeInfo? { let keys: [URLResourceKey] = [ .volumeNameKey, .volumeAvailableCapacityKey, .volumeAvailableCapacityForImportantUsageKey, .volumeTotalCapacityKey, .volumeIsRemovableKey, .volumeIsEjectableKey, ] guard let resource = try? url.resourceValues(forKeys: Set(keys)), let total = resource.volumeTotalCapacity, let availableWithPurgeable = resource.volumeAvailableCapacity, let realAvailable = resource.volumeAvailableCapacityForImportantUsage else { return nil } // Use regular available capacity if important usage capacity is 0 (common for DMGs) let effectiveAvailable = realAvailable > 0 ? Int(realAvailable) : availableWithPurgeable let finderTotalAvailable = Int64(effectiveAvailable) let realAvailableSpace = Int64(availableWithPurgeable) let purgeableSpace = max(0, finderTotalAvailable - realAvailableSpace) let realUsedSpace = Int64(total) - finderTotalAvailable let name = displayName ?? resource.volumeName ?? url.lastPathComponent let icon = NSWorkspace.shared.icon(forFile: url.path) icon.size = NSSize(width: 32, height: 32) // Debug prints // print("=== Volume: \(name) ===") // print("Total: \(ByteCountFormatter.string(fromByteCount: Int64(total), countStyle: .file))") // print("Available (with purgeable): \(ByteCountFormatter.string(fromByteCount: Int64(availableWithPurgeable), countStyle: .file))") // print("Available (important usage): \(ByteCountFormatter.string(fromByteCount: Int64(realAvailable), countStyle: .file))") // print("Calculated used: \(ByteCountFormatter.string(fromByteCount: realUsedSpace, countStyle: .file))") // print("Calculated purgeable: \(ByteCountFormatter.string(fromByteCount: purgeableSpace, countStyle: .file))") // print("========================") // Check if volume is external (removable or ejectable) let isRemovable = resource.volumeIsRemovable ?? false let isEjectable = resource.volumeIsEjectable ?? false let isExternal = isRemovable || isEjectable return VolumeInfo( name: name, path: url.path, icon: Image(nsImage: icon), totalSpace: Int64(total), usedSpace: realUsedSpace, realAvailableSpace: realAvailableSpace, purgeableSpace: purgeableSpace, isExternal: isExternal ) } private func isTimeMachineVolume(url: URL) -> Bool { let backupsPath = url.appendingPathComponent("Backups.backupdb") let tmDirectoryPath = url.appendingPathComponent(".com.apple.timemachine") // Check for common Time Machine indicators let hasBackupsDB = FileManager.default.fileExists(atPath: backupsPath.path) let hasTMDirectory = FileManager.default.fileExists(atPath: tmDirectoryPath.path) // Check if volume name contains "TimeMachine" let volumeName = url.lastPathComponent.lowercased() let isNamedTimeMachine = volumeName.contains("timemachine") || volumeName.contains("time machine") || volumeName.contains("time_machine") return hasBackupsDB || hasTMDirectory || isNamedTimeMachine } } struct VolumeInfo: Identifiable, Equatable { let id = UUID() let name: String let path: String let icon: Image let totalSpace: Int64 let usedSpace: Int64 let realAvailableSpace: Int64 let purgeableSpace: Int64 let isExternal: Bool var hasAnimated: Bool = false static func == (lhs: VolumeInfo, rhs: VolumeInfo) -> Bool { return lhs.id == rhs.id && lhs.name == rhs.name && lhs.path == rhs.path && lhs.totalSpace == rhs.totalSpace && lhs.usedSpace == rhs.usedSpace && lhs.realAvailableSpace == rhs.realAvailableSpace && lhs.purgeableSpace == rhs.purgeableSpace && lhs.isExternal == rhs.isExternal && lhs.hasAnimated == rhs.hasAnimated } } struct AppInfo: Identifiable, Equatable, Hashable { let id: UUID let path: URL let bundleIdentifier: String let appName: String let appVersion: String let appBuildNumber: String? let appIcon: NSImage? let webApp: Bool let wrapped: Bool let system: Bool var arch: Arch let cask: String? let steam: Bool // New property to mark Steam games let hasSparkle: Bool // Has Sparkle framework or Sparkle keys in Info.plist (detected at load time) let isAppStore: Bool // Has App Store receipt or iTunes metadata (detected at load time) let adamID: UInt64? // App Store adamID from mdls metadata (nil if not App Store or not indexed) let autoUpdates: Bool? // Homebrew cask auto_updates flag from cask JSON (nil if not Homebrew or unknown) var bundleSize: Int64 // Only used in the app list view var lipoSavings: Int64? // Cached lipo savings (nil=not calculated, 0=no savings, >0=savings available) var fileSize: [URL: Int64] // Logical file sizes (matches Finder) var fileIcon: [URL: NSImage?] let creationDate: Date? let contentChangeDate: Date? let lastUsedDate: Date? let dateAdded: Date? let entitlements: [String]? let teamIdentifier: String? var totalSize: Int64 { return fileSize.values.reduce(0, +) } var brew: Bool { return cask != nil } var executableURL: URL? { let infoPlistURL = path.appendingPathComponent("Contents/Info.plist") guard let info = NSDictionary(contentsOf: infoPlistURL) as? [String: Any], let execName = info["CFBundleExecutable"] as? String else { return nil } return path.appendingPathComponent("Contents/MacOS").appendingPathComponent(execName) } var averageColor: Color? { Color(appIcon?.averageColor ?? .clear) } var isEmpty: Bool { return path == URL(fileURLWithPath: "./") && bundleIdentifier.isEmpty && appName.isEmpty } static let empty = AppInfo( id: UUID(), path: URL(fileURLWithPath: ""), bundleIdentifier: "", appName: "", appVersion: "", appBuildNumber: nil, appIcon: nil, webApp: false, wrapped: false, system: false, arch: .empty, cask: nil, steam: false, hasSparkle: false, isAppStore: false, adamID: nil, autoUpdates: nil, bundleSize: 0, lipoSavings: 0, fileSize: [:], fileIcon: [:], creationDate: nil, contentChangeDate: nil, lastUsedDate: nil, dateAdded: nil, entitlements: nil, teamIdentifier: nil) } // MARK: - AppInfoMini (Phase 1 Fast Loading) /// Lightweight version of AppInfo for fast initial app list display /// Contains only essential properties needed for list rendering and sorting /// Converts to full AppInfo with placeholder values for deferred properties struct AppInfoMini { let id: UUID let path: URL let bundleIdentifier: String let appName: String let appVersion: String let appIcon: NSImage? let system: Bool let bundleSize: Int64 // Always calculated (mdls or totalSizeOnDisk) let creationDate: Date? let contentChangeDate: Date? let lastUsedDate: Date? let dateAdded: Date? /// Convert AppInfoMini to full AppInfo with placeholder values for expensive properties /// Phase 2 will populate these expensive properties in background func toAppInfo() -> AppInfo { return AppInfo( id: self.id, path: self.path, bundleIdentifier: self.bundleIdentifier, appName: self.appName, appVersion: self.appVersion, appBuildNumber: nil, // Phase 2 appIcon: self.appIcon, webApp: false, // Phase 2 wrapped: false, // Phase 2 system: self.system, arch: .empty, // Phase 2 (expensive) cask: nil, // Phase 2 (expensive) steam: false, // Phase 2 hasSparkle: false, // Phase 2 (expensive) isAppStore: false, // Phase 2 (expensive) adamID: nil, // Phase 2 autoUpdates: nil, // Phase 2 bundleSize: self.bundleSize, // ✅ Already calculated lipoSavings: nil, // Phase 2 fileSize: [:], // Populated when user selects app fileIcon: [:], // Populated when user selects app creationDate: self.creationDate, contentChangeDate: self.contentChangeDate, lastUsedDate: self.lastUsedDate, dateAdded: self.dateAdded, entitlements: nil, // Phase 2 (expensive) teamIdentifier: nil // Phase 2 (expensive) ) } } extension AppInfo { /// Convert full AppInfo to lightweight AppInfoMini (for fallback case when no mdls metadata) func toMini() -> AppInfoMini { return AppInfoMini( id: self.id, path: self.path, bundleIdentifier: self.bundleIdentifier, appName: self.appName, appVersion: self.appVersion, appIcon: self.appIcon, system: self.system, bundleSize: self.bundleSize, creationDate: self.creationDate, contentChangeDate: self.contentChangeDate, lastUsedDate: self.lastUsedDate, dateAdded: self.dateAdded ) } /// Generate debug string that excludes NSImage properties for cleaner output func getDebugString() -> String { // Format file paths with sizes let filePathsFormatted = fileSize .sorted { $0.key.path < $1.key.path } .map { " • \($0.key.path) (\(formatBytes($0.value)))" } .joined(separator: "\n") return """ ==================================== AppInfo Debug Output ==================================== ID: \(id) App Name: \(appName) Bundle ID: \(bundleIdentifier) Path: \(path.path) Version: \(appVersion) Build Number: \(appBuildNumber ?? "nil") ==================================== Architecture: \(arch) Web App: \(webApp) Wrapped: \(wrapped) System App: \(system) Steam Game: \(steam) Cask: \(cask ?? "nil") Auto Updates: \(autoUpdates ?? false) ==================================== Bundle Size: \(formatBytes(bundleSize)) Total Size: \(formatBytes(totalSize)) Lipo Savings: \(lipoSavings.map { formatBytes($0) } ?? "nil") ==================================== Creation Date: \(creationDate?.description ?? "nil") Content Change: \(contentChangeDate?.description ?? "nil") Last Used: \(lastUsedDate?.description ?? "nil") Date Added: \(dateAdded?.description ?? "nil") ==================================== Entitlements/Binaries: \(entitlements?.joined(separator: "\n") ?? "nil") ==================================== Team ID: \(teamIdentifier ?? "nil") ==================================== Update Sources: App Store: \(isAppStore) Homebrew: \(brew) Sparkle: \(hasSparkle) ==================================== Files (\(fileSize.count)): \(filePathsFormatted.isEmpty ? " (none)" : filePathsFormatted) ==================================== """ } } // MARK: - Fuzzy Search extension AppInfo: FuzzySearchable { var searchableString: String { return appName } } struct ZombieFile: Identifiable, Equatable, Hashable { let id: UUID var fileSize: [URL: Int64] // Logical file sizes (matches Finder) var fileIcon: [URL: NSImage?] var totalSize: Int64 { return fileSize.values.reduce(0, +) } static let empty = ZombieFile(id: UUID(), fileSize: [:], fileIcon: [:]) } struct AssociatedZombieFile: Codable { let appPath: URL // Unique identifier for the app let filePath: URL // The zombie file to be processed } class ZombieFileStorage { static let shared = ZombieFileStorage() var associatedFiles: [URL: [URL]] = [:] // Key: App Path, Value: Zombie File URLs // UserDefaults key for persistence private let associationsKey = "settings.general.zombie.associations" private init() { loadAssociations() } // Load associations from UserDefaults private func loadAssociations() { if let data = UserDefaults.standard.data(forKey: associationsKey), let storedAssociations = try? JSONDecoder().decode([String: [String]].self, from: data) { associatedFiles = storedAssociations.reduce(into: [URL: [URL]]()) { result, pair in let appURL = URL(fileURLWithPath: pair.key) let zombieURLs = pair.value.map { URL(fileURLWithPath: $0) } result[appURL] = zombieURLs } } } // Save associations to UserDefaults private func saveAssociations() { let storableAssociations = associatedFiles.reduce(into: [String: [String]]()) { result, pair in result[pair.key.path] = pair.value.map { $0.path } } if let encoded = try? JSONEncoder().encode(storableAssociations) { UserDefaults.standard.set(encoded, forKey: associationsKey) } } func addAssociation(appPath: URL, zombieFilePath: URL) { if associatedFiles[appPath] == nil { associatedFiles[appPath] = [] } if !associatedFiles[appPath]!.contains(zombieFilePath) { associatedFiles[appPath]?.append(zombieFilePath) saveAssociations() } } func getAssociatedFiles(for appPath: URL) -> [URL] { return associatedFiles[appPath] ?? [] } func isPathAssociated(_ path: URL) -> Bool { return associatedFiles.values.contains { $0.contains(path) } } func clearAssociations(for appPath: URL) { associatedFiles[appPath] = nil saveAssociations() } func removeAssociation(appPath: URL, zombieFilePath: URL) { guard var associatedFilesList = associatedFiles[appPath] else { return } associatedFilesList.removeAll { $0 == zombieFilePath } if associatedFilesList.isEmpty { associatedFiles.removeValue(forKey: appPath) // Remove key if no files are left } else { associatedFiles[appPath] = associatedFilesList } saveAssociations() } // Clean up associations for apps that no longer exist func cleanupInvalidAssociations(validAppPaths: [URL]) { let currentAppPaths = Set(associatedFiles.keys) let validAppPathsSet = Set(validAppPaths) let invalidPaths = currentAppPaths.subtracting(validAppPathsSet) for invalidPath in invalidPaths { associatedFiles.removeValue(forKey: invalidPath) } if !invalidPaths.isEmpty { saveAssociations() } } } enum Arch { case arm case intel case universal case empty var type: String { switch self { case .arm: return "arm" case .intel: return "intel" case .universal: return String(localized: "universal") case .empty: return "" } } } enum CurrentPage: Int, CaseIterable, Identifiable { case applications case development case fileSearch case homebrew case lipo case orphans case packages case plugins case services case updater var id: Int { rawValue } /// Pages that are hidden in release builds (only visible in DEBUG mode) static var debugOnlyPages: [CurrentPage] { return [] } /// Returns all pages filtered based on build configuration and user visibility settings static var availablePages: [CurrentPage] { let hiddenPages = AppState.loadHiddenPages() #if DEBUG return CurrentPage.allCases.filter { !hiddenPages.contains($0.rawValue) } #else return CurrentPage.allCases .filter { !debugOnlyPages.contains($0) } .filter { !hiddenPages.contains($0.rawValue) } #endif } var details: (title: String, icon: String) { switch self { case .applications: return (String(localized: "Apps"), "macwindow") case .development: return (String(localized: "Developer"), "hammer.circle") case .fileSearch: return (String(localized: "File Search"), "magnifyingglass") case .homebrew: return (String(localized: "Homebrew"), "mug") case .lipo: return (String(localized: "Lipo"), "scissors") case .orphans: return (String(localized: "Orphans"), "doc.text.magnifyingglass") case .packages: return (String(localized: "Packages"), "shippingbox") case .plugins: return (String(localized: "Plugins"), "puzzlepiece") case .services: return (String(localized: "Services"), "gearshape.2") case .updater: return (String(localized: "Updater"), "arrow.down.circle") } } var title: String { details.title } var icon: String { details.icon } } //MARK: Sorting for sidebar apps list enum SortOption: Int, CaseIterable, Identifiable { case alphabetical case size case creationDate case dateAdded case contentChangeDate case lastUsedDate var id: Int { rawValue } var title: String { let titles: [String] = [ String(localized: "App Name"), String(localized: "App Size"), String(localized: "Date Created"), String(localized: "Date Added"), String(localized: "Modified Date"), String(localized: "Last Used Date"), ] return titles[rawValue] } } //MARK: Sorting for file list view enum SortOptionList: String, CaseIterable { case name = "name" case path = "path" case size = "size" var title: String { switch self { case .name: return String(localized: "Name") case .path: return String(localized: "Path") case .size: return String(localized: "Size") } } var systemImage: String { switch self { case .name: return "textformat" case .path: return "folder" case .size: return "number" } } } enum CurrentTabView: Int, CaseIterable { case general case interface case folders case update case helper case about var title: String { switch self { case .general: return String(localized: "General") case .interface: return String(localized: "Interface") case .folders: return String(localized: "Folders") case .update: return String(localized: "Update") case .helper: return String(localized: "Helper") case .about: return String(localized: "About") } } } enum CurrentDetailsView: Int { case empty case files // case zombie } extension AppState { // Call this when switching to view an app to ensure associated files are loaded func loadAssociatedFilesForCurrentApp() { guard !appInfo.isEmpty else { return } let associatedFiles = ZombieFileStorage.shared.getAssociatedFiles(for: appInfo.path) for zombieFile in associatedFiles { if FileManager.default.fileExists(atPath: zombieFile.path) { // Add to current app's tracking if not already present if appInfo.fileSize[zombieFile] == nil { appInfo.fileSize[zombieFile] = 0 // Will be calculated during scan } } } } } ================================================ FILE: Pearcleaner/Logic/AppsUpdater/AppStoreReset.swift ================================================ // // AppStoreReset.swift // Pearcleaner // // Based on mas-cli's reset command // Copyright © 2018 mas-cli. All rights reserved. // License: MIT // import Foundation import AppKit import StoreFoundation import CommerceKit /// Manages App Store service reset operations /// Based on mas-cli's Reset.swift implementation /// Use this when App Store downloads are stuck or broken class AppStoreReset { /// Result of a reset operation enum ResetResult { case success case failure(String) } /// Resets the App Store by: /// 1. Terminating Dock and storeuid processes (handles App Store UI) /// 2. Killing Mac App Store system daemons /// 3. Deleting download cache directory /// /// - Returns: ResetResult indicating success or failure with error message static func reset() async -> ResetResult { // Step 1: Terminate App Store UI processes let terminatedApps = terminateAppStoreApps() // Step 2: Kill system daemons let killedDaemons = await killAppStoreDaemons() // Step 3: Delete download cache let deletedCache = deleteDownloadCache() // Check if all steps succeeded if terminatedApps && killedDaemons && deletedCache { return .success } else { var errors: [String] = [] if !terminatedApps { errors.append("Failed to terminate App Store apps") } if !killedDaemons { errors.append("Failed to kill system daemons") } if !deletedCache { errors.append("Failed to delete download cache") } return .failure(errors.joined(separator: ", ")) } } // MARK: - Private Implementation /// Terminates Dock and storeuid processes (handles App Store UI) private static func terminateAppStoreApps() -> Bool { let appNames = ["Dock", "storeuid"] var allTerminated = true for appName in appNames { // Find running instance guard let app = NSRunningApplication.runningApplications(withBundleIdentifier: "com.apple.\(appName)").first else { // App not running, skip (not an error) continue } // Terminate app if !app.terminate() { allTerminated = false } } return allTerminated } /// Kills Mac App Store system daemons using sysctl + proc_pidpath /// This is the low-level approach mas-cli uses (not using NSRunningApplication) private static func killAppStoreDaemons() async -> Bool { let daemonPaths = [ "/System/Library/PrivateFrameworks/CommerceKit.framework/Versions/A/Resources/storekitagent", "/System/Library/PrivateFrameworks/AppStoreDaemon.framework/Support/appstoreagent", "/System/Library/PrivateFrameworks/AppStoreDaemon.framework/Support/appstored", "/System/Library/PrivateFrameworks/CommerceKit.framework/Versions/Current/Resources/storekitagent", "/System/Library/PrivateFrameworks/CommerceKit.framework/Resources/storekitagent", "/System/Library/PrivateFrameworks/CommerceKit.framework/Versions/A/Resources/storeaccountd", "/System/Library/PrivateFrameworks/CommerceKit.framework/Versions/Current/Resources/storeaccountd", "/System/Library/PrivateFrameworks/CommerceKit.framework/Resources/storeaccountd", "/System/Library/PrivateFrameworks/CommerceKit.framework/Versions/A/Resources/storeassetd", "/System/Library/PrivateFrameworks/CommerceKit.framework/Versions/Current/Resources/storeassetd", "/System/Library/PrivateFrameworks/CommerceKit.framework/Resources/storeassetd", "/System/Library/PrivateFrameworks/CommerceKit.framework/Versions/A/Resources/storedownloadd", "/System/Library/PrivateFrameworks/CommerceKit.framework/Versions/Current/Resources/storedownloadd", "/System/Library/PrivateFrameworks/CommerceKit.framework/Resources/storedownloadd", "/System/Library/PrivateFrameworks/AppStoreDaemon.framework/Versions/A/Support/appstorecomponentsd", "/System/Library/PrivateFrameworks/AppStoreDaemon.framework/Support/appstorecomponentsd" ] var allKilled = true for daemonPath in daemonPaths { // Find PID for daemon path using sysctl if let pid = findPID(forExecutablePath: daemonPath) { // Kill process let result = kill(pid, SIGTERM) if result != 0 { allKilled = false } } // If daemon not running, that's fine (not an error) } return allKilled } /// Finds the PID for a given executable path using sysctl /// This is how mas-cli does it (low-level process enumeration) private static func findPID(forExecutablePath targetPath: String) -> pid_t? { // Get all process PIDs using sysctl var name: [Int32] = [CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0] var length: size_t = 0 // Get required buffer size if sysctl(&name, u_int(name.count), nil, &length, nil, 0) == -1 { return nil } // Allocate buffer let count = length / MemoryLayout.stride var procs = [kinfo_proc](repeating: kinfo_proc(), count: count) // Get process list if sysctl(&name, u_int(name.count), &procs, &length, nil, 0) == -1 { return nil } // Iterate through processes let actualCount = length / MemoryLayout.stride for i in 0.. 0 { let executablePath = String(cString: pathBuffer) if executablePath == targetPath { return pid } } } return nil } /// Deletes the App Store download cache using CommerceKit's CKDownloadDirectory() private static func deleteDownloadCache() -> Bool { // Get download directory from CommerceKit (mas-cli approach) let downloadDirPath = CKDownloadDirectory(nil) let fileManager = FileManager.default // Check if directory exists guard fileManager.fileExists(atPath: downloadDirPath) else { // Directory doesn't exist, nothing to delete (not an error) return true } // Delete directory do { try fileManager.removeItem(atPath: downloadDirPath) return true } catch { return false } } } ================================================ FILE: Pearcleaner/Logic/AppsUpdater/AppStoreUpdateChecker.swift ================================================ // // AppStoreUpdateChecker.swift // Pearcleaner // // Created by Alin Lupascu on 10/13/25. // import Foundation import CommerceKit import StoreFoundation import AlinFoundation class AppStoreUpdateChecker { private static let logger = UpdaterDebugLogger.shared /// Fallback regions to check if app not found in primary region /// Ordered by usage: CN, US, HK, JP, KR, SG private static let fallbackRegions = ["CN", "US", "HK", "JP", "KR", "SG"] /// Check if an app is a wrapped iPad/iOS app /// app.wrapped is already set correctly during app launch private static func isIOSApp(_ app: AppInfo) -> Bool { return app.wrapped } static func checkForUpdates(apps: [AppInfo]) async -> [UpdateableApp] { guard !apps.isEmpty else { return [] } logger.log(.appStore, "Starting App Store update check for \(apps.count) apps") await GlobalConsoleManager.shared.appendOutput("Checking for App Store updates (\(apps.count) apps)...\n", source: CurrentPage.updater.title) // Create optimal chunks based on CPU cores (smaller chunks for App Store API calls) let chunks = createOptimalChunks(from: apps, minChunkSize: 3, maxChunkSize: 10) // Process chunks concurrently using TaskGroup return await withTaskGroup(of: [UpdateableApp].self) { group in for chunk in chunks { group.addTask { await checkChunk(chunk: chunk) } } // Collect results from all chunks var allUpdates: [UpdateableApp] = [] for await chunkUpdates in group { // Check for cancellation between chunks if Task.isCancelled { break } allUpdates.append(contentsOf: chunkUpdates) } logger.log(.appStore, "Found \(allUpdates.count) available App Store updates") await GlobalConsoleManager.shared.appendOutput("Found \(allUpdates.count) App Store update(s)\n", source: CurrentPage.updater.title) return allUpdates } } /// Check a chunk of apps for updates concurrently private static func checkChunk(chunk: [AppInfo]) async -> [UpdateableApp] { await withTaskGroup(of: UpdateableApp?.self) { group in for app in chunk { group.addTask { await checkSingleApp(app: app) } } // Collect non-nil results var updates: [UpdateableApp] = [] for await update in group { if let update = update { updates.append(update) } } return updates } } /// Check a single app for updates private static func checkSingleApp(app: AppInfo) async -> UpdateableApp? { logger.log(.appStore, "Checking: \(app.appName) (\(app.bundleIdentifier))") // OPTIMIZATION: Try direct adamID lookup first if available (much faster than bundleID search) let result: (AppStoreInfo, String)? let isWrappedApp = isIOSApp(app) if let adamID = app.adamID { logger.log(.appStore, " 🚀 Fast path - using cached adamID: \(adamID)") let primaryRegion = await getAppStoreRegion() // Use entity filtering based on app type (prevents iOS version pollution for Mac apps) let entity = isWrappedApp ? "macSoftware" : "desktopSoftware" if let appStoreInfo = await fetchAppStoreInfoByAdamID(adamID: adamID, region: primaryRegion, entity: entity) { result = (appStoreInfo, primaryRegion) logger.log(.appStore, " ✅ Found via adamID lookup") } else { // Fallback to bundleID search if adamID lookup fails (app might have been transferred) logger.log(.appStore, " ⚠️ adamID lookup failed, falling back to bundleID search") result = await getAppStoreInfo(bundleID: app.bundleIdentifier, isWrappedIOSApp: isWrappedApp) } } else { // No cached adamID - use standard bundleID search logger.log(.appStore, " 📍 Standard path - using bundleID lookup") result = await getAppStoreInfo(bundleID: app.bundleIdentifier, isWrappedIOSApp: isWrappedApp) } guard let (appStoreInfo, foundRegion) = result else { logger.log(.appStore, " ❌ API lookup failed - not found in App Store") return nil } logger.log(.appStore, " ✅ Found in App Store: v\(appStoreInfo.version) (adamID: \(appStoreInfo.adamID)) in region: \(foundRegion)") // Use Version for robust comparison (handles 1, 2, 3+ component versions) let installedVer = Version(versionNumber: app.appVersion, buildNumber: nil) let availableVer = Version(versionNumber: appStoreInfo.version, buildNumber: nil) // Skip if versions are empty/invalid guard !installedVer.isEmpty && !availableVer.isEmpty else { logger.log(.appStore, " ⚠️ Skipped - empty/invalid version (Installed: \(app.appVersion), Available: \(appStoreInfo.version))") return nil } logger.log(.appStore, " Comparing versions - Installed: \(app.appVersion), Available: \(appStoreInfo.version)") // Detect if this is a wrapped iOS app let isIOSApp = Self.isIOSApp(app) // Only add if App Store version is GREATER than installed version if availableVer > installedVer { logger.log(.appStore, " 📦 UPDATE AVAILABLE: \(app.appVersion) → \(appStoreInfo.version)\(isIOSApp ? " (iOS app)" : "")") return UpdateableApp( appInfo: app, availableVersion: appStoreInfo.version, availableBuildNumber: nil, // App Store doesn't provide separate build numbers source: .appStore, adamID: appStoreInfo.adamID, appStoreURL: appStoreInfo.appStoreURL, status: .idle, progress: 0.0, isSelectedForUpdate: true, releaseTitle: nil, releaseDescription: appStoreInfo.releaseNotes, releaseNotesLink: nil, releaseDate: appStoreInfo.releaseDate, isPreRelease: false, // App Store updates are not pre-releases isIOSApp: isIOSApp, foundInRegion: foundRegion, fetchedReleaseNotes: nil, // App Store has inline release notes appcastItem: nil // App Store doesn't use Sparkle ) } logger.log(.appStore, " ✓ Up to date") return nil } private struct AppStoreInfo { let adamID: UInt64 let version: String let appStoreURL: String let releaseNotes: String? let releaseDate: String? } private static func getAppStoreInfo(bundleID: String, isWrappedIOSApp: Bool) async -> (info: AppStoreInfo, region: String)? { // Get user's primary region let primaryRegion = await getAppStoreRegion() logger.log(.appStore, " Primary region: \(primaryRegion)") // Try primary region first with all entity types if let info = await tryAllEntities(bundleID: bundleID, region: primaryRegion, isWrappedIOSApp: isWrappedIOSApp) { return (info, primaryRegion) } // If not found, try fallback regions logger.log(.appStore, " Not found in primary region, trying fallback regions...") for region in fallbackRegions where region != primaryRegion { logger.log(.appStore, " Trying region: \(region)") if let info = await tryAllEntities(bundleID: bundleID, region: region, isWrappedIOSApp: isWrappedIOSApp) { logger.log(.appStore, " ✓ Found in region: \(region)") return (info, region) } } logger.log(.appStore, " ❌ Not found in any region") return nil } /// Try all entity types for a given region based on app type private static func tryAllEntities(bundleID: String, region: String, isWrappedIOSApp: Bool) async -> AppStoreInfo? { if isWrappedIOSApp { // Wrapped iOS apps: Only try macSoftware (covers "Designed for iPad" apps) logger.log(.appStore, " Trying entity: macSoftware (iOS app)") if let info = await fetchAppStoreInfo(bundleID: bundleID, region: region, entity: "macSoftware") { logger.log(.appStore, " ✓ Found with macSoftware") return info } } else { // Regular Mac apps: Only try desktopSoftware (prevents iOS version pollution) logger.log(.appStore, " Trying entity: desktopSoftware (Mac app)") if let info = await fetchAppStoreInfo(bundleID: bundleID, region: region, entity: "desktopSoftware") { logger.log(.appStore, " ✓ Found with desktopSoftware") return info } } return nil } private static func fetchAppStoreInfo(bundleID: String, region: String, entity: String?) async -> AppStoreInfo? { // Query iTunes Search API using bundle ID guard let endpoint = URL(string: "https://itunes.apple.com/lookup") else { return nil } var components = URLComponents(url: endpoint, resolvingAgainstBaseURL: false) var queryItems = [ URLQueryItem(name: "bundleId", value: bundleID), URLQueryItem(name: "country", value: region), URLQueryItem(name: "limit", value: "1") ] // Add entity parameter if provided (desktopSoftware or macSoftware) if let entity = entity { queryItems.append(URLQueryItem(name: "entity", value: entity)) } components?.queryItems = queryItems guard let url = components?.url else { return nil } do { let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 30) let (data, _) = try await URLSession.shared.data(for: request) if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], let resultCount = json["resultCount"] as? Int, resultCount > 0, let results = json["results"] as? [[String: Any]], let firstResult = results.first, let trackId = firstResult["trackId"] as? UInt64, let version = firstResult["version"] as? String, let trackViewUrl = firstResult["trackViewUrl"] as? String { // Extract optional metadata let releaseNotes = firstResult["releaseNotes"] as? String let releaseDate = firstResult["currentVersionReleaseDate"] as? String return AppStoreInfo( adamID: trackId, version: version, appStoreURL: trackViewUrl, releaseNotes: releaseNotes, releaseDate: releaseDate ) } } catch { // Error querying iTunes API - silently fail } return nil } /// Fetch App Store info using adamID (faster than bundleID lookup) /// Uses direct adamID lookup, avoiding multi-region/entity fallback overhead private static func fetchAppStoreInfoByAdamID(adamID: UInt64, region: String, entity: String?) async -> AppStoreInfo? { // Query iTunes Search API using adamID guard let endpoint = URL(string: "https://itunes.apple.com/lookup") else { return nil } var components = URLComponents(url: endpoint, resolvingAgainstBaseURL: false) var queryItems = [ URLQueryItem(name: "id", value: String(adamID)), URLQueryItem(name: "country", value: region), URLQueryItem(name: "limit", value: "1") ] // Add entity parameter if provided (desktopSoftware for Mac apps, macSoftware for iOS apps) if let entity = entity { queryItems.append(URLQueryItem(name: "entity", value: entity)) } components?.queryItems = queryItems guard let url = components?.url else { return nil } do { let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData, timeoutInterval: 30) let (data, _) = try await URLSession.shared.data(for: request) if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], let resultCount = json["resultCount"] as? Int, resultCount > 0, let results = json["results"] as? [[String: Any]], let firstResult = results.first, let trackId = firstResult["trackId"] as? UInt64, let version = firstResult["version"] as? String, let trackViewUrl = firstResult["trackViewUrl"] as? String { // Extract optional metadata let releaseNotes = firstResult["releaseNotes"] as? String let releaseDate = firstResult["currentVersionReleaseDate"] as? String return AppStoreInfo( adamID: trackId, version: version, appStoreURL: trackViewUrl, releaseNotes: releaseNotes, releaseDate: releaseDate ) } } catch { // Error querying iTunes API - silently fail } return nil } // MARK: - Private Helpers /// Get the user's App Store region (2-letter ISO 3166-1 alpha-2 code) /// Locale.region.identifier already returns alpha-2 codes (e.g., "US", "GB", "FR") private static func getAppStoreRegion() async -> String { return Locale.autoupdatingCurrent.region?.identifier ?? "US" } } ================================================ FILE: Pearcleaner/Logic/AppsUpdater/AppStoreUpdater.swift ================================================ // // AppStoreUpdater.swift // Pearcleaner // // Created by Alin Lupascu on 10/13/25. // import Foundation import CommerceKit import StoreFoundation import AlinFoundation // MARK: - Error Types enum AppStoreUpdateError: Error { case noDownloads case downloadFailed(String) case downloadCancelled case networkError(Error) } // MARK: - AppStoreUpdater class AppStoreUpdater { static let shared = AppStoreUpdater() private init() {} /// Check if running macOS version affected by installd bug /// Affected: 14.8.2 (Darwin 24.6.2), 15.7.2 (Darwin 25.7.2), and 26.1+ (Darwin 26.1+) private func needsInstalldWorkaround() -> Bool { let version = ProcessInfo.processInfo.operatingSystemVersion // macOS 26.1+ (all versions) if version.majorVersion >= 26 && version.minorVersion >= 1 { return true } // macOS 15.7.2 (Darwin 25.7.2) if version.majorVersion == 25 && version.minorVersion == 7 && version.patchVersion >= 2 { return true } // macOS 14.8.2 (Darwin 24.6.2) if version.majorVersion == 24 && version.minorVersion == 6 && version.patchVersion >= 2 { return true } return false } /// Update an app from the App Store with progress tracking /// - Parameters: /// - adamID: The App Store ID of the app /// - appPath: Path to the installed app (for receipt injection) /// - progress: Progress callback (percent: 0.0-1.0, status message) /// - attemptCount: Number of retry attempts for network errors (default: 3) func updateApp( adamID: UInt64, appPath: URL, isIOSApp: Bool = false, progress: @escaping @Sendable (Double, String) -> Void, attemptCount: UInt32 = 3 ) async throws { await GlobalConsoleManager.shared.appendOutput("Initiating App Store download for adamID \(adamID)...\n", source: CurrentPage.updater.title) do { // Create SSPurchase for downloading (purchasing: false = update existing app) // NOTE: For iOS apps, all entity types ("software", "macSoftware", "desktopSoftware") // download the same universal variant, which is NOT Mac-compatible. // iOS app updates are currently non-functional due to Apple's CDN serving wrong variant. let purchase = await SSPurchase(adamID: adamID, purchasing: false, kind: "software") // Mark as update to indicate this is a redownload of owned app purchase.isUpdate = true // iOS apps need special handling on ALL macOS versions (flag passed from caller) // Check if workaround is needed for macOS apps on affected OS versions let needsWorkaround = needsInstalldWorkaround() if isIOSApp { await GlobalConsoleManager.shared.appendOutput("Detected iOS app - using custom installer\n", source: CurrentPage.updater.title) } else if needsWorkaround { await GlobalConsoleManager.shared.appendOutput("Detected macOS version with installer bug - using custom installer\n", source: CurrentPage.updater.title) } try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in // NOTE: iOS apps will show "Current Version Not Compatible" dialog // User must click "Download Last Compatible" to proceed with download CKPurchaseController.shared().perform(purchase, withOptions: 0) { _, _, error, response in if let error = error { Task { await GlobalConsoleManager.shared.appendOutput("✗ App Store purchase failed: \(error.localizedDescription)\n", source: CurrentPage.updater.title) } continuation.resume(throwing: error) } else if response?.downloads?.isEmpty == false { // Download started - create observer to track it Task { await GlobalConsoleManager.shared.appendOutput("Download started, monitoring progress...\n", source: CurrentPage.updater.title) do { if isIOSApp { // iOS apps: Use dedicated iOS observer (all macOS versions) printOS("⚠️ Detected iOS app - using custom installer") let observer = IOSDownloadObserver(adamID: adamID, appPath: appPath, progress: progress) try await observer.observeDownloadQueue() } else if needsWorkaround { // macOS apps on affected OS versions: Use workaround observer printOS("⚠️ Detected macOS version with private frameworks installer bug - using custom installer") let observer = MacOSDownloadObserverWithWorkaround(adamID: adamID, appPath: appPath, progress: progress) try await observer.observeDownloadQueue() } else { // macOS apps on unaffected OS versions: Use standard observer let observer = AppStoreDownloadObserver(adamID: adamID, progress: progress) try await observer.observeDownloadQueue() } await GlobalConsoleManager.shared.appendOutput("Download and installation completed successfully\n", source: CurrentPage.updater.title) continuation.resume() } catch { await GlobalConsoleManager.shared.appendOutput("✗ Download/installation failed: \(error.localizedDescription)\n", source: CurrentPage.updater.title) continuation.resume(throwing: error) } } } else { // No downloads means already up to date Task { await GlobalConsoleManager.shared.appendOutput("App is already up to date\n", source: CurrentPage.updater.title) } progress(1.0, "Already up to date") continuation.resume() } } } } catch { // Retry logic for network errors (like mas does) guard attemptCount > 1 else { await GlobalConsoleManager.shared.appendOutput("✗ Update failed after retries: \(error.localizedDescription)\n", source: CurrentPage.updater.title) throw error } // Only retry network errors guard (error as NSError).domain == NSURLErrorDomain else { await GlobalConsoleManager.shared.appendOutput("✗ Non-network error, not retrying: \(error.localizedDescription)\n", source: CurrentPage.updater.title) throw error } let remainingAttempts = attemptCount - 1 await GlobalConsoleManager.shared.appendOutput("Network error, retrying... (\(remainingAttempts) attempts remaining)\n", source: CurrentPage.updater.title) try await updateApp(adamID: adamID, appPath: appPath, isIOSApp: isIOSApp, progress: progress, attemptCount: remainingAttempts) } } } // MARK: - AppStoreDownloadObserver /// Per-download observer that tracks a single App Store download/update /// This matches the architecture used by mas CLI tool private final class AppStoreDownloadObserver: NSObject, CKDownloadQueueObserver { private let adamID: UInt64 private let progressCallback: @Sendable (Double, String) -> Void private var completionHandler: (() -> Void)? private var errorHandler: ((Error) -> Void)? init(adamID: UInt64, progress: @escaping @Sendable (Double, String) -> Void) { self.adamID = adamID self.progressCallback = progress super.init() } /// Observe the download queue until this download completes /// Uses defer to ensure observer is always removed when done func observeDownloadQueue(_ queue: CKDownloadQueue = .shared()) async throws { let observerID = queue.add(self) defer { queue.removeObserver(observerID) } try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in completionHandler = { [weak self] in self?.completionHandler = nil self?.errorHandler = nil continuation.resume() } errorHandler = { [weak self] error in self?.completionHandler = nil self?.errorHandler = nil continuation.resume(throwing: error) } } } // MARK: - CKDownloadQueueObserver Delegate Methods func downloadQueue(_ queue: CKDownloadQueue, changedWithAddition download: SSDownload) { // Download was added to queue - no action needed } func downloadQueue(_ queue: CKDownloadQueue, changedWithRemoval download: SSDownload) { guard let metadata = download.metadata, metadata.itemIdentifier == adamID, let status = download.status else { return } // This is the official completion signal from CommerceKit if status.isFailed { let error = status.error ?? AppStoreUpdateError.downloadFailed("Download failed") Task { await GlobalConsoleManager.shared.appendOutput("✗ App Store download failed: \(error.localizedDescription)\n", source: CurrentPage.updater.title) } errorHandler?(error) } else if status.isCancelled { Task { await GlobalConsoleManager.shared.appendOutput("Download cancelled by user\n", source: CurrentPage.updater.title) } errorHandler?(AppStoreUpdateError.downloadCancelled) } else { // Success! Task { await GlobalConsoleManager.shared.appendOutput("App Store download completed\n", source: CurrentPage.updater.title) } progressCallback(1.0, "Completed") completionHandler?() } } func downloadQueue(_ queue: CKDownloadQueue, statusChangedFor download: SSDownload) { guard let metadata = download.metadata, metadata.itemIdentifier == adamID, let status = download.status, let activePhase = status.activePhase else { return } let phaseType = activePhase.phaseType let percentComplete = status.percentComplete // Float: 0.0 to 1.0 let progress = max(0.0, min(1.0, Double(percentComplete))) // Report progress based on phase // Special case: at 100%, always show "Installing..." (CommerceKit sometimes resets to phase 0 at completion) if progress >= 1.0 { progressCallback(progress, "Installing...") } else { switch phaseType { case 0: // Downloading progressCallback(progress, "Downloading...") case 1: // Installing progressCallback(progress, "Installing...") case 4: // Initial/Preparing progressCallback(progress, "Preparing...") case 5: // Downloaded (not complete yet - wait for changedWithRemoval) progressCallback(progress, "Installing...") default: progressCallback(progress, "Processing...") } } } } // MARK: - IOSDownloadObserver /// Observer for iOS/iPad app downloads (IPA files) /// Preserves IPA to /tmp for installation /// Used for all iOS apps regardless of macOS version private final class IOSDownloadObserver: NSObject, CKDownloadQueueObserver { private let adamID: UInt64 private let appPath: URL private let progressCallback: @Sendable (Double, String) -> Void private var completionHandler: (() -> Void)? private var errorHandler: ((Error) -> Void)? private var iosFilesPreserved = false // Track if IPA was already preserved private var hardLinkedIPAPath: String? // Path to hard-linked IPA in /tmp private var isManuallyInstalling = false init(adamID: UInt64, appPath: URL, progress: @escaping @Sendable (Double, String) -> Void) { self.adamID = adamID self.appPath = appPath self.progressCallback = progress super.init() } func observeDownloadQueue(_ queue: CKDownloadQueue = .shared()) async throws { let observerID = queue.add(self) defer { queue.removeObserver(observerID) } try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in completionHandler = { [weak self] in self?.completionHandler = nil self?.errorHandler = nil continuation.resume() } errorHandler = { [weak self] error in self?.completionHandler = nil self?.errorHandler = nil continuation.resume(throwing: error) } } } // MARK: - CKDownloadQueueObserver Delegate Methods func downloadQueue(_ queue: CKDownloadQueue, changedWithAddition download: SSDownload) { // Download was added to queue - no action needed } func downloadQueue(_ queue: CKDownloadQueue, changedWithRemoval download: SSDownload) { guard let metadata = download.metadata, metadata.itemIdentifier == adamID, let status = download.status else { return } // iOS app download completed - always perform manual installation if IPA was preserved // (CommerceKit cannot install iOS apps, so we handle it ourselves regardless of reported status) if let ipaPath = hardLinkedIPAPath { Task { await performManualInstallation(ipaPath: ipaPath) } } else { // No IPA preserved - this shouldn't happen, but handle gracefully if status.isFailed || status.isCancelled { errorHandler?(AppStoreUpdateError.downloadFailed("Failed to preserve IPA file")) } else { // Unexpected: CommerceKit claims success but we don't have an IPA printOS("⚠️ Download completed but no IPA was preserved") progressCallback(1.0, "Completed") completionHandler?() } } } func downloadQueue(_ queue: CKDownloadQueue, statusChangedFor download: SSDownload) { guard let metadata = download.metadata, metadata.itemIdentifier == adamID, let status = download.status, let activePhase = status.activePhase else { return } let phaseType = activePhase.phaseType let percentComplete = status.percentComplete let progress = max(0.0, min(1.0, Double(percentComplete))) // At 80% progress, preserve IPA file before CommerceKit potentially cleans it up if progress >= 0.80 && progress < 1.0 && !iosFilesPreserved { preserveIPAFile() } // Report progress if isManuallyInstalling { progressCallback(progress, "Installing...") } else if progress >= 1.0 { progressCallback(progress, "Downloading...") } else { switch phaseType { case 0: // Downloading progressCallback(progress, "Downloading...") case 4: // Initial/Preparing progressCallback(progress, "Preparing...") default: progressCallback(progress, "Downloading...") } } } // MARK: - Helper Methods private func performManualInstallation(ipaPath: String) async { isManuallyInstalling = true // 80%: Preparing installation progressCallback(0.80, "Preparing installation...") try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 sec do { // 80-100%: IOSAppInstaller handles the rest (extraction, metadata, installation, cleanup) try await IOSAppInstaller.installIOSApp( ipaPath: ipaPath, adamID: adamID, existingAppPath: appPath, progress: progressCallback ) completionHandler?() } catch { errorHandler?(error) } } private func preserveIPAFile() { guard !iosFilesPreserved else { return } let downloadDir = "\(CKDownloadDirectory(nil))/\(adamID)" let tempDir = "/tmp/pearcleaner-ios-\(adamID)" do { // Create temp directory try FileManager.default.createDirectory(atPath: tempDir, withIntermediateDirectories: true) let contents = try FileManager.default.contentsOfDirectory(atPath: downloadDir) if let ipaFile = contents.first(where: { $0.hasSuffix(".ipa") }) { // Hard link IPA to /tmp (same pattern as PKG files) let ipaSource = "\(downloadDir)/\(ipaFile)" let ipaDest = "\(tempDir)/app.ipa" try FileManager.default.linkItem(atPath: ipaSource, toPath: ipaDest) hardLinkedIPAPath = ipaDest iosFilesPreserved = true } else { printOS("⚠️ No IPA file found in \(downloadDir)") } } catch { printOS("❌ Failed to preserve IPA: \(error.localizedDescription)") } } } // MARK: - MacOSDownloadObserverWithWorkaround /// Special observer for macOS versions affected by installd bug /// Downloads PKG, hard links it, then manually installs via HelperToolManager /// Used only for macOS apps on affected OS versions private final class MacOSDownloadObserverWithWorkaround: NSObject, CKDownloadQueueObserver { private let adamID: UInt64 private let appPath: URL private let progressCallback: @Sendable (Double, String) -> Void private var completionHandler: (() -> Void)? private var errorHandler: ((Error) -> Void)? private var hardLinkedPkgPath: String? private var hardLinkedReceiptPath: String? private var isManuallyInstalling = false init(adamID: UInt64, appPath: URL, progress: @escaping @Sendable (Double, String) -> Void) { self.adamID = adamID self.appPath = appPath self.progressCallback = progress super.init() } func observeDownloadQueue(_ queue: CKDownloadQueue = .shared()) async throws { let observerID = queue.add(self) defer { queue.removeObserver(observerID) } try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in completionHandler = { [weak self] in self?.completionHandler = nil self?.errorHandler = nil continuation.resume() } errorHandler = { [weak self] error in self?.completionHandler = nil self?.errorHandler = nil continuation.resume(throwing: error) } } } // MARK: - CKDownloadQueueObserver Delegate Methods func downloadQueue(_ queue: CKDownloadQueue, changedWithAddition download: SSDownload) { // Download was added to queue - no action needed } func downloadQueue(_ queue: CKDownloadQueue, changedWithRemoval download: SSDownload) { guard let metadata = download.metadata, metadata.itemIdentifier == adamID, let status = download.status else { return } // Download completed (installd will fail, but we have the PKG hard linked) if status.isFailed || status.isCancelled { // Expected failure on affected macOS versions - proceed with manual installation if let pkgPath = hardLinkedPkgPath { Task { await performManualInstallation(pkgPath: pkgPath) } } else { errorHandler?(AppStoreUpdateError.downloadFailed("Failed to preserve PKG file")) } } else { // Unexpected success (installd worked somehow) if let pkgPath = hardLinkedPkgPath { // Clean up temp directory since installd succeeded let tempDir = (pkgPath as NSString).deletingLastPathComponent try? FileManager.default.removeItem(atPath: tempDir) hardLinkedPkgPath = nil hardLinkedReceiptPath = nil } progressCallback(1.0, "Completed") completionHandler?() } } func downloadQueue(_ queue: CKDownloadQueue, statusChangedFor download: SSDownload) { guard let metadata = download.metadata, metadata.itemIdentifier == adamID, let status = download.status, let activePhase = status.activePhase else { return } let phaseType = activePhase.phaseType let percentComplete = status.percentComplete let progress = max(0.0, min(1.0, Double(percentComplete))) // At 80% progress, create hard link to preserve PKG before installd fails if progress >= 0.80 && progress < 1.0 && hardLinkedPkgPath == nil && !isManuallyInstalling { createHardLinkToPKG() } // Report progress if isManuallyInstalling { progressCallback(progress, "Installing...") } else if progress >= 1.0 { progressCallback(progress, "Downloading...") } else { switch phaseType { case 0: // Downloading progressCallback(progress, "Downloading...") case 4: // Initial/Preparing progressCallback(progress, "Preparing...") default: progressCallback(progress, "Downloading...") } } } // MARK: - Helper Methods private func createHardLinkToPKG() { let downloadDir = "\(CKDownloadDirectory(nil))/\(adamID)" let tempDir = "/tmp/pearcleaner-appstore-\(adamID)" do { // Create temp directory try FileManager.default.createDirectory(atPath: tempDir, withIntermediateDirectories: true) let contents = try FileManager.default.contentsOfDirectory(atPath: downloadDir) // Find PKG file (macOS apps only) if let pkgFile = contents.first(where: { $0.hasSuffix(".pkg") }) { // Hard link PKG file let pkgSource = "\(downloadDir)/\(pkgFile)" let pkgDest = "\(tempDir)/\(pkgFile)" try FileManager.default.linkItem(atPath: pkgSource, toPath: pkgDest) hardLinkedPkgPath = pkgDest // Hard link receipt file let receiptSource = "\(downloadDir)/receipt" let receiptDest = "\(tempDir)/receipt" if FileManager.default.fileExists(atPath: receiptSource) { try FileManager.default.linkItem(atPath: receiptSource, toPath: receiptDest) hardLinkedReceiptPath = receiptDest } else { printOS("⚠️ No receipt file found in \(downloadDir)") } } else { printOS("⚠️ No PKG file found in \(downloadDir)") } } catch { printOS("❌ Failed to create hard links: \(error.localizedDescription)") } } private func performManualInstallation(pkgPath: String) async { isManuallyInstalling = true // 80-85%: Preparing installation progressCallback(0.80, "Preparing installation...") try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 sec progressCallback(0.85, "Installing...") // 85-90%: Running installer (Script 1) let installerScript = "installer -pkg '\(pkgPath)' -target /" let result = try! await runSUCommand( installerScript, errorContext: "Failed to install package", throwOnFailure: false ) progressCallback(0.90, "Configuring App Store receipt...") // 90-95%: Inject receipt and refresh metadata (Script 2 - only runs if Script 1 succeeds) if result.0, let receiptPath = hardLinkedReceiptPath, FileManager.default.fileExists(atPath: receiptPath) { let appPathString = appPath.path let receiptDir = "\(appPathString)/Contents/_MASReceipt" let receiptDestPath = "\(receiptDir)/receipt" // Create receipt directory, copy receipt, set permissions, and set ownership to root:wheel (chained commands) let receiptScript = """ mkdir -p '\(receiptDir)' && \ cp '\(receiptPath)' '\(receiptDestPath)' && \ chmod 644 '\(receiptDestPath)' && \ chown 0:0 '\(receiptDestPath)' """ let receiptResult = try! await runSUCommand( receiptScript, errorContext: "Failed to configure App Store receipt", throwOnFailure: false ) if receiptResult.0 { // Force immediate Spotlight re-indexing let mdimportProcess = Process() mdimportProcess.executableURL = URL(fileURLWithPath: "/usr/bin/mdimport") mdimportProcess.arguments = ["-i", appPathString] try? mdimportProcess.run() mdimportProcess.waitUntilExit() } } progressCallback(0.95, "Cleaning up...") // 95-100%: Clean up temp directory if let pkgPath = hardLinkedPkgPath { let tempDir = (pkgPath as NSString).deletingLastPathComponent try? FileManager.default.removeItem(atPath: tempDir) } if result.0 { progressCallback(1.0, "Completed") completionHandler?() } else { printOS("❌ Manual PKG installation failed: \(result.1)") errorHandler?(AppStoreUpdateError.downloadFailed("Installation failed: \(result.1)")) } } } ================================================ FILE: Pearcleaner/Logic/AppsUpdater/HomebrewAdoption.swift ================================================ // // HomebrewAdoption.swift // Pearcleaner // // Created by Alin Lupascu on 11/18/25. // import Foundation // MARK: - Adoptable Cask Model struct AdoptableCask: Identifiable { let id: String // token let token: String let displayName: String let description: String? let version: String let autoUpdates: Bool let homepage: String? let isVersionCompatible: Bool let matchScore: Int init(from searchResult: HomebrewSearchResult, appInfo: AppInfo, matchScore: Int) { self.token = searchResult.name self.id = searchResult.name self.displayName = searchResult.displayName ?? searchResult.name self.description = searchResult.description self.version = searchResult.version ?? "unknown" self.autoUpdates = searchResult.autoUpdates ?? false self.homepage = searchResult.homepage self.matchScore = matchScore // Version compatibility check if self.autoUpdates { // Auto-updating apps don't need strict version matching self.isVersionCompatible = true } else { // Non auto-updating apps need version match self.isVersionCompatible = appInfo.appVersion.pearFormat() == self.version.pearFormat() } } } // MARK: - Cask Matching Logic /// Finds matching Homebrew casks for a given app /// Returns array of AdoptableCask sorted by match score (highest first) func findMatchingCasks(for appInfo: AppInfo, from casks: [HomebrewSearchResult]) -> [AdoptableCask] { var matches: [(cask: HomebrewSearchResult, score: Int)] = [] let appNameNormalized = appInfo.appName.pearFormat() let bundleIdNormalized = appInfo.bundleIdentifier.pearFormat() for cask in casks { var score = 0 // Skip deprecated/disabled casks if cask.isDeprecated || cask.isDisabled { continue } // Priority 1: Check artifacts array (e.g., ["1Password.app"]) if let artifacts = cask.artifacts { for artifact in artifacts { let artifactNormalized = artifact.pearFormat() // Exact match with .app extension if artifactNormalized == appNameNormalized + "app" { score += 100 break } // Match without extension if artifactNormalized == appNameNormalized { score += 90 break } // Partial match if artifactNormalized.contains(appNameNormalized) || appNameNormalized.contains(artifactNormalized) { score += 50 } } } // Priority 2: Check caskName array (e.g., ["1Password", "One Password"]) if let caskNames = cask.caskName { for name in caskNames { let nameNormalized = name.pearFormat() // Exact match if nameNormalized == appNameNormalized { score += 80 break } // Partial match (exact match only for very short names ≤2 chars) if nameNormalized.count <= 2 || appNameNormalized.count <= 2 { // Exact match only for very short names to avoid false positives (e.g., "R" matching "Apparency") // Skip partial matching } else { // Partial match for longer names (3+ characters) if nameNormalized.contains(appNameNormalized) || appNameNormalized.contains(nameNormalized) { score += 40 } } } } // Priority 3: Check displayName (human-readable name) if let displayName = cask.displayName { let displayNameNormalized = displayName.pearFormat() // Exact match if displayNameNormalized == appNameNormalized { score += 70 } // Partial match (exact match only for very short names ≤2 chars) if displayNameNormalized.count <= 2 || appNameNormalized.count <= 2 { // Exact match only for very short names to avoid false positives (e.g., "R" matching "Apparency") // Skip partial matching } else { // Partial match for longer names (3+ characters) if displayNameNormalized.contains(appNameNormalized) || appNameNormalized.contains(displayNameNormalized) { score += 35 } } } // Priority 4: Check token (cask identifier, e.g., "1password") let tokenNormalized = cask.name.pearFormat() // Exact match if tokenNormalized == appNameNormalized { score += 60 } // Partial match (exact match only for very short names ≤2 chars) if tokenNormalized.count <= 2 || appNameNormalized.count <= 2 { // Exact match only for very short names to avoid false positives (e.g., "R" matching "Apparency") // Skip partial matching } else { // Partial match for longer names (3+ characters) if tokenNormalized.contains(appNameNormalized) || appNameNormalized.contains(tokenNormalized) { score += 30 } } // Priority 5: Check bundle ID if available in description or homepage // Some casks include bundle ID in description or we can infer from paths if !bundleIdNormalized.isEmpty { if let description = cask.description { let descNormalized = description.pearFormat() if descNormalized.contains(bundleIdNormalized) { score += 25 } } } // Only include casks with meaningful matches (score > 0) if score > 0 { matches.append((cask: cask, score: score)) } } // Sort by score descending, then by name ascending matches.sort { lhs, rhs in if lhs.score != rhs.score { return lhs.score > rhs.score } return lhs.cask.name < rhs.cask.name } // Convert to AdoptableCask and return return matches.map { AdoptableCask(from: $0.cask, appInfo: appInfo, matchScore: $0.score) } } /// Validates if a manually entered cask token exists in the cask database /// Returns AdoptableCask if found, nil otherwise func validateManualCaskEntry(_ token: String, for appInfo: AppInfo, from casks: [HomebrewSearchResult]) -> AdoptableCask? { let tokenNormalized = token.pearFormat() // Find exact token match guard let cask = casks.first(where: { $0.name.pearFormat() == tokenNormalized }) else { return nil } // Skip deprecated/disabled casks if cask.isDeprecated || cask.isDisabled { return nil } // Return as AdoptableCask with manual entry score return AdoptableCask(from: cask, appInfo: appInfo, matchScore: 999) // High score for manual entry } ================================================ FILE: Pearcleaner/Logic/AppsUpdater/HomebrewUpdateChecker.swift ================================================ // // HomebrewUpdateChecker.swift // Pearcleaner // // Created by Alin Lupascu on 10/13/25. // import Foundation class HomebrewUpdateChecker { static func checkForUpdates(apps: [AppInfo], includeFormulae: Bool, showAutoUpdatesInHomebrew: Bool = true) async -> [UpdateableApp] { await GlobalConsoleManager.shared.appendOutput("Checking for Homebrew updates...\n", source: CurrentPage.updater.title) // Filter apps that have a cask identifier // When showAutoUpdatesInHomebrew=false: exclude casks with auto_updates=true (they'll only appear in Sparkle) let brewApps = apps.filter { app in guard let cask = app.cask, !cask.isEmpty else { return false } // If user disabled auto-updating apps in Homebrew section, filter them out if !showAutoUpdatesInHomebrew, let autoUpdates = app.autoUpdates, autoUpdates { return false // Skip apps with auto_updates=true } return true } await GlobalConsoleManager.shared.appendOutput("Found \(brewApps.count) Homebrew-managed apps to check\n", source: CurrentPage.updater.title) // Step 1: Build list of installed packages by scanning Cellar/Caskroom (~70ms) // This enables the fast hybrid API approach instead of slow `brew outdated` command var installedCasks: [InstalledPackage] = [] var installedFormulae: [InstalledPackage] = [] // Scan casks (always needed) try? await HomebrewController.shared.streamInstalledPackages(cask: true) { name, displayName, desc, version, isPinned, tap, tapRbPath, installedOnRequest in // Check for cancellation during streaming if Task.isCancelled { return } installedCasks.append(InstalledPackage( name: name, displayName: displayName, description: desc, version: version, isCask: true, isPinned: isPinned, tap: tap, tapRbPath: tapRbPath, installedOnRequest: installedOnRequest // Always true for casks )) } // Check for cancellation before scanning formulae if Task.isCancelled { return [] } // Scan formulae only if enabled if includeFormulae { try? await HomebrewController.shared.streamInstalledPackages(cask: false) { name, displayName, desc, version, isPinned, tap, tapRbPath, installedOnRequest in // Check for cancellation during streaming if Task.isCancelled { return } installedFormulae.append(InstalledPackage( name: name, displayName: displayName, description: desc, version: version, isCask: false, isPinned: isPinned, tap: tap, tapRbPath: tapRbPath, installedOnRequest: installedOnRequest )) } } // Step 2: Check outdated using FAST hybrid API approach (~650ms) // Much faster than `brew outdated` command (~2.3s) - uses parallel API calls + .rb fallback for taps await GlobalConsoleManager.shared.appendOutput("Checking outdated packages (scanning \(installedCasks.count) casks", source: CurrentPage.updater.title) if includeFormulae { await GlobalConsoleManager.shared.appendOutput(", \(installedFormulae.count) formulae", source: CurrentPage.updater.title) } await GlobalConsoleManager.shared.appendOutput(")...\n", source: CurrentPage.updater.title) let outdatedPackages = await HomebrewController.shared.getOutdatedPackagesHybrid( formulae: installedFormulae, casks: installedCasks ) guard !outdatedPackages.isEmpty else { await GlobalConsoleManager.shared.appendOutput("All Homebrew packages are up to date\n", source: CurrentPage.updater.title) return [] } await GlobalConsoleManager.shared.appendOutput("Found \(outdatedPackages.count) outdated package(s)\n", source: CurrentPage.updater.title) var updateableApps: [UpdateableApp] = [] // Process outdated casks (GUI apps) - no API calls needed, versions already included for outdatedPkg in outdatedPackages where outdatedPkg.isCask { // Find matching app by cask name guard let appInfo = brewApps.first(where: { $0.cask == outdatedPkg.name }) else { continue } // Use app's ACTUAL version from Info.plist (ground truth) instead of Homebrew's stale record // This eliminates false positives for auto-updating apps (Sparkle, direct downloads) let installedClean = appInfo.appVersion.stripBrewRevisionSuffix() let availableClean = outdatedPkg.availableVersion.stripBrewRevisionSuffix() // Compare ACTUAL app version vs latest Homebrew version let installed = Version(versionNumber: installedClean, buildNumber: nil) let available = Version(versionNumber: availableClean, buildNumber: nil) // Only add if truly outdated (available > installed) // Homebrew's .metadata/ record is ignored for comparison (can be stale if app auto-updated) guard !installed.isEmpty && !available.isEmpty && available > installed else { continue // Skip if versions are equal, invalid, or installed is newer } // Keep actual version from Info.plist for accurate UI display // (Comparison above already used this version, so UI should match) let correctedAppInfo = AppInfo( id: appInfo.id, path: appInfo.path, bundleIdentifier: appInfo.bundleIdentifier, appName: appInfo.appName, appVersion: appInfo.appVersion, // Use ACTUAL version from Info.plist (ground truth) appBuildNumber: appInfo.appBuildNumber, appIcon: appInfo.appIcon, webApp: appInfo.webApp, wrapped: appInfo.wrapped, system: appInfo.system, arch: appInfo.arch, cask: appInfo.cask, steam: appInfo.steam, hasSparkle: appInfo.hasSparkle, isAppStore: appInfo.isAppStore, adamID: appInfo.adamID, autoUpdates: appInfo.autoUpdates, bundleSize: appInfo.bundleSize, lipoSavings: appInfo.lipoSavings, fileSize: appInfo.fileSize, fileIcon: appInfo.fileIcon, creationDate: appInfo.creationDate, contentChangeDate: appInfo.contentChangeDate, lastUsedDate: appInfo.lastUsedDate, dateAdded: appInfo.dateAdded, entitlements: appInfo.entitlements, teamIdentifier: appInfo.teamIdentifier ) let updateableApp = UpdateableApp( appInfo: correctedAppInfo, // Use version from Homebrew metadata availableVersion: outdatedPkg.availableVersion, // Available version from brew outdated availableBuildNumber: nil, // Homebrew doesn't provide separate build numbers source: .homebrew, adamID: nil, appStoreURL: nil, status: .idle, progress: 0.0, isSelectedForUpdate: true, releaseTitle: nil, releaseDescription: nil, releaseNotesLink: nil, releaseDate: nil, isPreRelease: false, // Homebrew updates are not pre-releases isIOSApp: false, // Homebrew apps are never iOS apps foundInRegion: nil, fetchedReleaseNotes: nil, // Homebrew doesn't provide release notes appcastItem: nil // Homebrew doesn't use Sparkle ) updateableApps.append(updateableApp) } // Process outdated formulae (CLI tools) if enabled - no API calls needed if includeFormulae { // Get the set of cask names we've already processed let processedCasks = Set(brewApps.compactMap { $0.cask }) for outdatedPkg in outdatedPackages where !outdatedPkg.isCask { // Skip if this formula name matches a cask we already processed guard !processedCasks.contains(outdatedPkg.name) else { continue } // Clean versions for comparison (same logic as casks) let installedClean = outdatedPkg.installedVersion.stripBrewRevisionSuffix() let availableClean = outdatedPkg.availableVersion.stripBrewRevisionSuffix() // Compare versions using Version struct let installed = Version(versionNumber: installedClean, buildNumber: nil) let available = Version(versionNumber: availableClean, buildNumber: nil) // Only add if truly outdated (available > installed) // This filters out revision bumps where user-facing version is identical guard !installed.isEmpty && !available.isEmpty && available > installed else { continue // Skip if versions are equal, invalid, or installed is newer } // Create a minimal AppInfo for the formula (CLI tool) let formulaAppInfo = AppInfo( id: UUID(), path: URL(fileURLWithPath: "\(HomebrewController.shared.brewPrefix)/bin/\(outdatedPkg.name)"), // Placeholder path bundleIdentifier: "com.homebrew.formula.\(outdatedPkg.name)", appName: outdatedPkg.name, appVersion: outdatedPkg.installedVersion, appBuildNumber: nil, // Formulae don't have build numbers appIcon: nil, webApp: false, wrapped: false, system: false, arch: .universal, cask: outdatedPkg.name, // Store formula name in cask field for update/uninstall operations steam: false, hasSparkle: false, // Formulae (CLI tools) don't have Sparkle isAppStore: false, // Formulae are not from App Store adamID: nil, // Formulae are not from App Store autoUpdates: nil, // Formulae don't have auto_updates (cask-only property) bundleSize: 0, lipoSavings: nil, fileSize: [:], fileIcon: [:], creationDate: nil, contentChangeDate: nil, lastUsedDate: nil, dateAdded: nil, entitlements: nil, teamIdentifier: nil ) let updateableApp = UpdateableApp( appInfo: formulaAppInfo, availableVersion: outdatedPkg.availableVersion, availableBuildNumber: nil, // Homebrew doesn't provide separate build numbers source: .homebrew, adamID: nil, appStoreURL: nil, status: .idle, progress: 0.0, isSelectedForUpdate: true, releaseTitle: nil, releaseDescription: nil, releaseNotesLink: nil, releaseDate: nil, isPreRelease: false, isIOSApp: false, foundInRegion: nil, appcastItem: nil // Homebrew doesn't use Sparkle ) updateableApps.append(updateableApp) } } await GlobalConsoleManager.shared.appendOutput("Processed \(updateableApps.count) updateable app(s) from Homebrew\n", source: CurrentPage.updater.title) return updateableApps } } ================================================ FILE: Pearcleaner/Logic/AppsUpdater/IOSAppInstaller.swift ================================================ // // IOSAppInstaller.swift // Pearcleaner // // Created by Alin Lupascu on 11/12/25. // import Foundation import AppKit import AlinFoundation // MARK: - Supporting Types struct IOSVersionInfo { let version: String let build: String let bundleID: String } struct IOSPreservedMetadata { let protectedMetadata: Data let itemId: Int64? let artistName: String? let purchaseDate: Date? let appleId: String? let storefrontCountryCode: String? let releaseDate: String? let genre: String? let rawPlist: [String: Any] // Keep entire original for safe merge } enum IOSAppInstallerError: Error, LocalizedError { case noAppBundleFound case extractionFailed(String) case invalidInfoPlist case noExistingInstallation case missingProtectedMetadata case apiLookupFailed case atomicReplacementFailed(String) var errorDescription: String? { switch self { case .noAppBundleFound: return "No .app bundle found in IPA Payload" case .extractionFailed(let details): return "IPA extraction failed: \(details)" case .invalidInfoPlist: return "Invalid Info.plist in app bundle" case .noExistingInstallation: return "No existing app installation found" case .missingProtectedMetadata: return "Missing protectedMetadata in existing installation" case .apiLookupFailed: return "Failed to fetch app metadata from iTunes API" case .atomicReplacementFailed(let details): return "Atomic replacement failed: \(details)" } } } // MARK: - IOSAppInstaller class IOSAppInstaller { /// Install or update an iOS app from IPA file static func installIOSApp( ipaPath: String, adamID: UInt64, existingAppPath: URL, progress: @escaping (Double, String) -> Void ) async throws { // Normalize path to outer wrapper at entry point (defensive coding) let normalizedAppPath = normalizeToOuterWrapper(existingAppPath) // 1. Extract IPA to temp directory (80-85%) progress(0.80, "Installing...") let extractedPayload = try await extractIPA(ipaPath: ipaPath, adamID: adamID) // 2. Detect wrapped bundle name (NOT hardcoded!) let wrappedBundleName = try detectWrappedBundleName(payloadDir: extractedPayload) let extractedApp = extractedPayload.appendingPathComponent(wrappedBundleName) // 3. Read new version info from extracted app let versionInfo = try readVersionInfo(from: extractedApp) // 4. Preserve critical metadata from existing installation (85%) progress(0.85, "Installing...") let preservedData = try preserveExistingMetadata(appPath: normalizedAppPath) // 5. Generate updated metadata files let newITunesMetadata = try await generateITunesMetadata( adamID: adamID, versionInfo: versionInfo, preservedData: preservedData ) let newBundleMetadata = try generateBundleMetadata() // 6. Build new Wrapper structure in temp let newWrapper = try buildWrapperStructure( extractedApp: extractedApp, iTunesMetadata: newITunesMetadata, bundleMetadata: newBundleMetadata, adamID: adamID ) // 7. Atomic replacement with root privileges (90%) progress(0.90, "Installing...") try await performAtomicReplacement( existingAppPath: normalizedAppPath, newWrapper: newWrapper, wrappedBundleName: wrappedBundleName ) // 8. Cleanup (95%) progress(0.95, "Installing...") // Remove entire temp directory (includes extracted IPA and hard link) let tempDir = "/tmp/pearcleaner-ios-\(adamID)" if FileManager.default.fileExists(atPath: tempDir) { try? FileManager.default.removeItem(atPath: tempDir) } progress(1.0, "Completed") } // MARK: - Private Helper Methods /// Normalize path to outer wrapper (handles both inner app and outer wrapper paths) /// - Parameter appPath: Either inner app or outer wrapper path /// - Returns: Path to outer wrapper private static func normalizeToOuterWrapper(_ appPath: URL) -> URL { // Check if this is already the outer wrapper by looking for Wrapper/ subdirectory let wrapperDir = appPath.appendingPathComponent("Wrapper") let isOuterWrapper = FileManager.default.fileExists(atPath: wrapperDir.path) if isOuterWrapper { // Already at outer wrapper return appPath } else { // This is the inner app, go up two levels to outer wrapper // /Applications/To Do List.app/Wrapper/ToDoList.app -> /Applications/To Do List.app return appPath.deletingLastPathComponent().deletingLastPathComponent() } } /// Detect the wrapped bundle name dynamically (e.g., "Runner.app", "DREO.app") private static func detectWrappedBundleName(payloadDir: URL) throws -> String { let contents = try FileManager.default.contentsOfDirectory( at: payloadDir, includingPropertiesForKeys: nil ) // Find first .app bundle in Payload directory guard let appBundle = contents.first(where: { $0.pathExtension == "app" }) else { throw IOSAppInstallerError.noAppBundleFound } return appBundle.lastPathComponent } /// Extract IPA to temp directory private static func extractIPA(ipaPath: String, adamID: UInt64) async throws -> URL { let extractDir = URL(fileURLWithPath: "/tmp/pearcleaner-ios-\(adamID)/extracted") // Create extraction directory try FileManager.default.createDirectory(at: extractDir, withIntermediateDirectories: true) // Extract using ditto (handles LZFSE compression) let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/bin/ditto") process.arguments = ["-x", "-k", ipaPath, extractDir.path] let outputPipe = Pipe() let errorPipe = Pipe() process.standardOutput = outputPipe process.standardError = errorPipe try process.run() process.waitUntilExit() if process.terminationStatus != 0 { let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() let errorString = String(data: errorData, encoding: .utf8) ?? "Unknown error" throw IOSAppInstallerError.extractionFailed(errorString) } // Return Payload directory path return extractDir.appendingPathComponent("Payload") } /// Read version info from app bundle private static func readVersionInfo(from appPath: URL) throws -> IOSVersionInfo { let infoPlistPath = appPath.appendingPathComponent("Info.plist") let infoPlistData = try Data(contentsOf: infoPlistPath) let plist = try PropertyListSerialization.propertyList( from: infoPlistData, format: nil ) as! [String: Any] guard let version = plist["CFBundleShortVersionString"] as? String, let build = plist["CFBundleVersion"] as? String, let bundleID = plist["CFBundleIdentifier"] as? String else { throw IOSAppInstallerError.invalidInfoPlist } return IOSVersionInfo(version: version, build: build, bundleID: bundleID) } /// Preserve critical metadata from existing installation private static func preserveExistingMetadata(appPath: URL) throws -> IOSPreservedMetadata { // Normalize to outer wrapper first (handles both inner app and outer wrapper paths) let normalizedPath = normalizeToOuterWrapper(appPath) let metadataPath = normalizedPath .appendingPathComponent("Wrapper") .appendingPathComponent("iTunesMetadata.plist") guard FileManager.default.fileExists(atPath: metadataPath.path) else { throw IOSAppInstallerError.noExistingInstallation } let data = try Data(contentsOf: metadataPath) let plist = try PropertyListSerialization.propertyList( from: data, format: nil ) as! [String: Any] // Extract all fields that must be preserved guard let protectedMetadata = plist["protectedMetadata"] as? Data else { throw IOSAppInstallerError.missingProtectedMetadata } return IOSPreservedMetadata( protectedMetadata: protectedMetadata, itemId: plist["itemId"] as? Int64, artistName: plist["artistName"] as? String, purchaseDate: plist["purchaseDate"] as? Date, appleId: plist["appleId"] as? String, storefrontCountryCode: plist["storefrontCountryCode"] as? String, releaseDate: plist["releaseDate"] as? String, genre: plist["genre"] as? String, rawPlist: plist // Keep entire original for safe merge ) } /// Generate updated iTunesMetadata.plist private static func generateITunesMetadata( adamID: UInt64, versionInfo: IOSVersionInfo, preservedData: IOSPreservedMetadata ) async throws -> [String: Any] { // Fetch latest metadata from iTunes API let url = URL(string: "https://itunes.apple.com/lookup?id=\(adamID)")! let (data, _) = try await URLSession.shared.data(from: url) let json = try JSONSerialization.jsonObject(with: data) as! [String: Any] let results = json["results"] as! [[String: Any]] guard let _ = results.first else { throw IOSAppInstallerError.apiLookupFailed } // Start with preserved metadata (keeps ALL original fields) var metadata = preservedData.rawPlist // Update ONLY version-specific fields metadata["bundleShortVersionString"] = versionInfo.version metadata["bundleVersion"] = versionInfo.build // NOTE: Do NOT update softwareVersionExternalIdentifier - it must remain as integer from original // The preserved rawPlist already contains the correct integer value // CRITICAL: Ensure protectedMetadata is preserved metadata["protectedMetadata"] = preservedData.protectedMetadata return metadata } /// Generate BundleMetadata.plist in NSKeyedArchiver format /// Must match the exact structure that App Store creates to avoid launch failures private static func generateBundleMetadata() throws -> Data { let installDate = Date().timeIntervalSinceReferenceDate let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/bin/sw_vers") process.arguments = ["-buildVersion"] let pipe = Pipe() process.standardOutput = pipe try process.run() process.waitUntilExit() let data = pipe.fileHandleForReading.readDataToEndOfFile() let buildVersion = String(data: data, encoding: .utf8)? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "25B78" // Create NSKeyedArchiver format matching App Store's structure // This is critical - plain dict format causes kLSInvalidWrapperErr (-10671) let archivedDict: [String: Any] = [ "$archiver": "NSKeyedArchiver", "$version": 100000, "$objects": [ "$null", // Index 0 [ // Index 1 - root object (MIBundleMetadata) "$class": ["CF$UID": 6], "installDate": ["CF$UID": 2], "installBuildVersion": ["CF$UID": 4], "installType": ["CF$UID": 5], "autoInstallOverride": ["CF$UID": 5], "alternateIconName": ["CF$UID": 0], "placeholderFailureReason": ["CF$UID": 5], "placeholderFailureUnderlyingError": ["CF$UID": 0], "placeholderFailureUnderlyingErrorSource": ["CF$UID": 5], "watchKitAppExecutableHash": ["CF$UID": 0] ], [ // Index 2 - NSDate object "$class": ["CF$UID": 3], "NS.time": installDate ], [ // Index 3 - NSDate class "$classes": ["NSDate", "NSObject"], "$classname": "NSDate" ], buildVersion, // Index 4 0, // Index 5 [ // Index 6 - MIBundleMetadata class "$classes": ["MIBundleMetadata", "NSObject"], "$classname": "MIBundleMetadata" ] ], "$top": [ "root": ["CF$UID": 1] ] ] return try PropertyListSerialization.data( fromPropertyList: archivedDict, format: .binary, options: 0 ) } /// Build new Wrapper structure in temp directory private static func buildWrapperStructure( extractedApp: URL, iTunesMetadata: [String: Any], bundleMetadata: Data, // Changed from [String: Any] to Data adamID: UInt64 ) throws -> URL { let wrapperDir = URL(fileURLWithPath: "/tmp/pearcleaner-ios-\(adamID)/Wrapper") try FileManager.default.createDirectory(at: wrapperDir, withIntermediateDirectories: true) // Copy extracted app to Wrapper/ let appName = extractedApp.lastPathComponent try FileManager.default.copyItem( at: extractedApp, to: wrapperDir.appendingPathComponent(appName) ) // Write metadata plists (both must be binary format) let iTunesData = try PropertyListSerialization.data( fromPropertyList: iTunesMetadata, format: .binary, // CRITICAL: Must be binary, not XML options: 0 ) try iTunesData.write(to: wrapperDir.appendingPathComponent("iTunesMetadata.plist")) // BundleMetadata is already in binary NSKeyedArchiver format try bundleMetadata.write(to: wrapperDir.appendingPathComponent("BundleMetadata.plist")) return wrapperDir } /// Perform atomic replacement using unified privileged command wrapper private static func performAtomicReplacement( existingAppPath: URL, newWrapper: URL, wrappedBundleName: String ) async throws { // Normalize to outer wrapper first (handles both inner app and outer wrapper paths) let normalizedPath = normalizeToOuterWrapper(existingAppPath) let oldWrapper = normalizedPath.appendingPathComponent("Wrapper") let backupWrapper = URL(fileURLWithPath: "/tmp/pearcleaner-ios-backup-\(UUID().uuidString)") let symlinkPath = normalizedPath.appendingPathComponent("WrappedBundle") // Script 1: Atomic replacement (all commands chained with &&) let installScript = """ pkill -x '\(wrappedBundleName.replacingOccurrences(of: ".app", with: ""))' 2>/dev/null || true && \ mv '\(oldWrapper.path)' '\(backupWrapper.path)' && \ mv '\(newWrapper.path)' '\(oldWrapper.path)' && \ chown -R root:wheel '\(normalizedPath.path)' && \ chmod -R 755 '\(normalizedPath.path)' && \ rm -f '\(symlinkPath.path)' && \ ln -s 'Wrapper/\(wrappedBundleName)' '\(symlinkPath.path)' && \ rm -rf '\(backupWrapper.path)' """ let result = try await runSUCommand( installScript, errorContext: "Failed to install iOS app", throwOnFailure: false ) // Script 2: Restore on failure (only runs if Script 1 fails) if !result.0 { printOS("Atomic replacement failed, attempting to restore backup...") if FileManager.default.fileExists(atPath: backupWrapper.path) { let restoreScript = "mv '\(backupWrapper.path)' '\(oldWrapper.path)'" let _ = try await runSUCommand( restoreScript, errorContext: "Failed to restore backup after failed installation" ) } throw IOSAppInstallerError.atomicReplacementFailed(result.1) } } } ================================================ FILE: Pearcleaner/Logic/AppsUpdater/Models.swift ================================================ // // Models.swift // Pearcleaner // // Created by Alin Lupascu on 10/13/25. // import Foundation import SwiftUI import Sparkle enum UpdateSource: String, CaseIterable { case homebrew = "Homebrew" case appStore = "App Store" case sparkle = "Sparkle" case unsupported = "Unsupported" case current = "Current" var icon: String { switch self { case .homebrew: return "🍺" case .appStore: return "􀎶" case .sparkle: return "✨" case .unsupported: return "❓" case .current: return "✓" } } } enum UpdateStatus: Equatable { case idle case checking case downloading case extracting case installing case verifying case completed case failed(String) static func == (lhs: UpdateStatus, rhs: UpdateStatus) -> Bool { switch (lhs, rhs) { case (.idle, .idle), (.checking, .checking), (.downloading, .downloading), (.extracting, .extracting), (.installing, .installing), (.verifying, .verifying), (.completed, .completed): return true case (.failed(let lhsMsg), .failed(let rhsMsg)): return lhsMsg == rhsMsg default: return false } } } struct UpdateableApp: Identifiable, Hashable { let id = UUID() let appInfo: AppInfo var availableVersion: String? var availableBuildNumber: String? // Remote build number (CFBundleVersion) for smart version display let source: UpdateSource let adamID: UInt64? let appStoreURL: String? // Store URL from iTunes API for "Open in App Store" button var status: UpdateStatus = .idle var progress: Double = 0.0 var isSelectedForUpdate: Bool = true // Default to selected for "Update All" queue // Sparkle/App Store metadata (optional) let releaseTitle: String? let releaseDescription: String? let releaseNotesLink: String? // URL to external release notes page let releaseDate: String? let isPreRelease: Bool // True for Sparkle pre-release updates (has channel tag or SemVer pre-release identifier) let isIOSApp: Bool // True for wrapped iPad/iOS apps that must be updated via App Store app let foundInRegion: String? // App Store region code where update was found (e.g., "US", "CN") var fetchedReleaseNotes: String? // Fetched content from external releaseNotesLink URL // Cached Sparkle appcast item (for consistent version selection between check and install) let appcastItem: SUAppcastItem? var canUpdate: Bool { source == .homebrew || source == .appStore } /// Unique identifier for tracking hidden apps var uniqueIdentifier: String { appInfo.bundleIdentifier } // Hashable conformance func hash(into hasher: inout Hasher) { hasher.combine(id) } static func == (lhs: UpdateableApp, rhs: UpdateableApp) -> Bool { lhs.id == rhs.id } } // Conform to FuzzySearchable for automatic fuzzy search support in GenericSidebarListView extension UpdateableApp: FuzzySearchable { var searchableString: String { return appInfo.appName } } ================================================ FILE: Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/CFBundle/CFBundle_Private.h ================================================ // // CFBundle_Private.h // Pearcleaner // // Private API for flushing bundle caches // #import /*! @abstract Clears cache values on the given bundle object. @discussion This is private API and is subject to change in future OS versions. Check for availability prior to usage. Source: https://michelf.ca/blog/2010/killer-private-eraser/ */ extern void _CFBundleFlushBundleCaches(CFBundleRef bundle) __attribute__((weak_import)); ================================================ FILE: Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/CommerceKit/CKDownloadDirectory.h ================================================ // // CKDownloadDirectory.h // mas // // Copyright © 2018 mas-cli. All rights reserved. // NSString * _Nonnull CKDownloadDirectory(NSString * _Nullable target); ================================================ FILE: Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/CommerceKit/CKDownloadQueue.h ================================================ // // Generated by https://github.com/blacktop/ipsw (Version: 3.1.626, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26, SDK: 26, Tool: ld (1220.3) // - LC_SOURCE_VERSION: 716.0.8.0.0 // @interface CKDownloadQueue : CKServiceInterface { NSMutableDictionary *_downloadsByItemID; NSLock *_downloadsLock; id _observerToken NS_AVAILABLE_MAC(13); NSLock *_tokenLock NS_AVAILABLE_MAC(13); } + (nonnull instancetype)sharedDownloadQueue; @property (retain, nonatomic, nullable) NSMutableDictionary *downloadQueueObservers; // @property (readonly, nonatomic, nullable) NSArray *downloads; // Unverified generic type @property (retain, nonatomic, nullable) CKDownloadQueueClient *sharedObserver; - (void)addDownload:(nonnull SSDownload *)download; - (nonnull NSString *)addObserver:(nullable id )observer; - (nonnull NSString *)addObserver:(nullable id )observer forDownloadTypes:(long long)downloadTypes; - (nonnull NSString *)addObserverForDownloadTypes:(long long)downloadTypes withBlock:(nullable UnknownBlock)block; - (BOOL)cacheReceiptDataForDownload:(nullable SSDownload *)download; - (void)cancelDownload:(nullable SSDownload *)download promptToConfirm:(BOOL)promptToConfirm askToDelete:(BOOL)askToDelete; - (void)checkStoreDownloadQueueForAccount:(nullable ISStoreAccount *)account; // Unverified account type - (void)connectionWasInterrupted; - (nullable SSDownload *)downloadForItemIdentifier:(unsigned long long)identifier; // Unverified return type - (void)fetchIconForItemIdentifier:(unsigned long long)identifier atURL:(nullable NSURL *)url replyBlock:(nonnull UnknownBlock)block; - (nonnull instancetype)initWithStoreClient:(nullable ISStoreClient *)client; // Unverified client type - (void)lockApplicationsForBundleID:(nullable NSString *)bundleID; // Unverified bundleID type - (void)lockedApplicationTriedToLaunchAtPath:(nullable NSString *)path; // Unverified path type - (void)pauseDownloadWithItemIdentifier:(unsigned long long)identifier; - (void)performedIconAnimationForDownloadWithIdentifier:(unsigned long long)identifier; - (void)removeDownloadWithItemIdentifier:(unsigned long long)identifier; - (void)removeObserver:(nullable NSString *)observer; // Unverified observer type - (void)resumeDownloadWithItemIdentifier:(unsigned long long)identifier; - (void)unlockApplicationsWithBundleIdentifier:(nullable NSString *)bundleID; // Unverified bundleID type @end ================================================ FILE: Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/CommerceKit/CKDownloadQueueObserver-Protocol.h ================================================ // // CKDownloadQueueObserver-Protocol.h // mas // // Copyright © 2018 mas-cli. All rights reserved. // @protocol CKDownloadQueueObserver @required - (void)downloadQueue:(nonnull CKDownloadQueue *)queue changedWithAddition:(nonnull SSDownload *)download; - (void)downloadQueue:(nonnull CKDownloadQueue *)queue changedWithRemoval:(nonnull SSDownload *)download; - (void)downloadQueue:(nonnull CKDownloadQueue *)queue statusChangedForDownload:(nonnull SSDownload *)download; @optional @end ================================================ FILE: Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/CommerceKit/CKPurchaseController.h ================================================ // // Generated by https://github.com/blacktop/ipsw (Version: 3.1.626, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26, SDK: 26, Tool: ld (1220.3) // - LC_SOURCE_VERSION: 716.0.8.0.0 // typedef void (^SSPurchaseCompletion)(SSPurchase * _Nonnull purchase, BOOL completed, NSError * _Nullable error, SSPurchaseResponse * _Nullable response); @interface CKPurchaseController : CKServiceInterface { NSArray *_adoptionEligibleItems; NSNumber *_adoptionErrorNumber; NSNumber *_adoptionServerStatus; NSMutableArray *_purchases; NSMutableArray *_rejectedPurchases; } + (void)setNeedsSilentMachineAuthorization:(BOOL)needsSilentMachineAuthorization; + (nonnull instancetype)sharedPurchaseController; @property (copy, nullable) void (^dialogHandler)(CKDialog * _Nullable); // Unverified type - (void)_performVPPReceiptRenewal; - (BOOL)adoptionCompletedForBundleID:(nullable NSString *)bundleID; - (void)cancelPurchaseWithProductID:(nullable NSNumber *)productID; - (void)checkServerDownloadQueue; - (void)performPurchase:(nonnull SSPurchase *)purchase withOptions:(unsigned long long)options completionHandler:(nullable SSPurchaseCompletion)handler; - (nullable SSPurchase *)purchaseInProgressForProductID:(nullable NSNumber *)productID; // Unverified return type - (nullable NSArray *)purchasesInProgress; // Unverified return type - (void)resumeDownloadForPurchasedProductID:(nullable NSNumber *)productID; // Unverified productID type - (void)startPurchases:(nullable NSArray *)purchases shouldStartDownloads:(BOOL)shouldStartDownloads eventHandler:(nullable void (^)(NSArray * _Nonnull))handler; // Unverified purchases generic type / handler type - (void)startPurchases:(nullable NSArray *)purchases withOptions:(unsigned long long)options completionHandler:(nullable void (^)(NSArray * _Nonnull))handler; // Unverified purchases type / handler parameter type @end ================================================ FILE: Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/CommerceKit/CKServiceInterface.h ================================================ // // Generated by https://github.com/blacktop/ipsw (Version: 3.1.626, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26, SDK: 26, Tool: ld (1220.3) // - LC_SOURCE_VERSION: 716.0.8.0.0 // @interface CKServiceInterface : ISServiceProxy @end ================================================ FILE: Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/CommerceKit/CommerceKit.h ================================================ // // Generated by https://github.com/blacktop/ipsw (Version: 3.1.626, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26, SDK: 26, Tool: ld (1220.3) // - LC_SOURCE_VERSION: 716.0.8.0.0 // @import StoreFoundation; @class CKDialog, CKDownloadQueueClient; @protocol CKDownloadQueueObserver; #import "CKServiceInterface.h" #import "CKDownloadDirectory.h" #import "CKDownloadQueue.h" #import "CKDownloadQueueObserver-Protocol.h" #import "CKPurchaseController.h" ================================================ FILE: Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/CommerceKit/module.modulemap ================================================ module CommerceKit [system] [no_undeclared_includes] { requires macos, objc use StoreFoundation link framework "CommerceKit" umbrella header "CommerceKit.h" export * } ================================================ FILE: Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/StoreFoundation/ISAccountService-Protocol.h ================================================ // // Generated by https://github.com/blacktop/ipsw (Version: 3.1.626, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26, SDK: 26, Tool: ld (1220.3) // - LC_SOURCE_VERSION: 716.0.8.0.0 // @protocol ISAccountService @required - (void)accountWithAppleID:(nullable NSString *)appleID replyBlock:(nonnull void (^)(ISStoreAccount * _Nullable))block; // Unverified appleID type / block parameter types - (void)accountWithDSID:(nullable NSNumber *)dsID replyBlock:(nonnull void (^)(ISStoreAccount * _Nullable))block; // Unverified dsID type / block parameter types - (void)addAccount:(nullable ISStoreAccount *)account; // Unverified account type - (void)addAccountStoreObserver:(nullable id )observer; // Unverified observer type - (void)addAccountWithAuthenticationResponse:(nullable ISAuthenticationResponse *)authenticationResponse makePrimary:(BOOL)makePrimary replyBlock:(nonnull void (^)(ISStoreAccount * _Nullable))block NS_DEPRECATED_MAC(10_9, 12); // Unverified - (void)addURLBagObserver:(nullable id )observer; - (void)authIsExpiredWithReplyBlock:(nonnull void (^)(BOOL))block; // Unverified block parameter types - (void)dictionaryForDSID:(nullable NSNumber *)dsID withReplyBlock:(nonnull void (^)(NSDictionary * _Nullable))block NS_AVAILABLE_MAC(13); // Unverified dsID type / block parameter types - (void)dictionaryWithReplyBlock:(nonnull void (^)(NSDictionary * _Nonnull))block; // Unverified block parameter types - (void)generateTouchIDHeadersForDSID:(nullable NSNumber *)dsID challenge:(nullable NSString *)challenge caller:(nullable id)caller replyBlock:(nonnull void (^)(NSDictionary * _Nonnull, NSError * _Nullable))block; // Unverified dsID type / challenge type / caller type / block parameter types - (void)getTouchIDPreferenceWithReplyBlock:(nonnull void (^)(BOOL, ISStoreAccount * _Nullable, NSError * _Nullable))block; // Unverified block parameter types - (void)httpHeadersForURL:(nullable NSURL *)url forDSID:(nullable NSNumber *)dsID includeADIHeaders:(BOOL)includeADIHeaders withReplyBlock:(nonnull void (^)(NSDictionary * _Nonnull))block; // Unverified url type / dsID type / block parameter types - (void)iCloudDSIDReplyBlock:(nonnull void (^)(NSString * _Nullable))block; // Unverified block parameter types - (void)invalidateAllBags; - (void)isValidWithReplyBlock:(nonnull void (^)(BOOL))block; // Unverified block parameter types - (void)loadURLBagWithType:(unsigned long long)type replyBlock:(nonnull void (^)(BOOL, BOOL, NSError * _Nullable))block; // Unverified block parameter types - (void)needsSilentADIActionForURL:(nullable NSURL *)url dsID:(nullable NSNumber *)dsID withReplyBlock:(nonnull void (^)(BOOL))block NS_AVAILABLE_MAC(13); // Unverified url type / dsID type / block parameter types - (void)needsSilentADIActionForURL:(nullable NSURL *)url withReplyBlock:(nonnull void (^)(BOOL))block; // Unverified url type / block parameter types - (void)parseCreditStringForProtocol:(nullable NSDictionary *)dictionary NS_DEPRECATED_MAC(10_9, 12); // Unverified dictionary type - (void)primaryAccountWithReplyBlock:(nonnull void (^)(ISStoreAccount * _Nullable))block; // Unverified block parameter types - (void)processURLResponse:(nullable NSURLResponse *)urlResponse forRequest:(nullable NSURLRequest *)request; // Unverified urlResponse type / request type - (void)processURLResponse:(nullable NSURLResponse *)urlResponse forRequest:(nullable NSURLRequest *)request dsID:(nullable NSNumber *)dsID NS_AVAILABLE_MAC(13); // Unverified urlResponse type / request type / dsID type - (void)recommendedAppleIDForAccountSignIn:(nonnull void (^)(NSString * _Nullable))appleID NS_DEPRECATED_MAC(10_9, 12); // Unverified - (void)regexWithKey:(nullable NSString *)key dsID:(nullable NSNumber *)dsID matchesString:(nullable NSString *)string replyBlock:(nonnull void (^)(BOOL))block NS_AVAILABLE_MAC(13); // Unverified key type / dsID type / string type / block parameter types - (void)regexWithKey:(nullable NSString *)key matchesString:(nullable NSString *)string replyBlock:(nonnull void (^)(BOOL))block; // Unverified key type / string type / block parameter types - (void)removeAccountStoreObserver:(nullable id )observer; // Unverified observer type - (void)removeURLBagObserver:(nullable id )observer; // Unverified observer type - (void)retailStoreDemoModeReplyBlock:(nonnull void (^)(BOOL, NSString * _Nullable, NSString * _Nullable, BOOL))block; // Unverified block parameter types - (void)setStoreFrontID:(nullable NSString *)storefrontID; // Unverified storefrontID type - (void)setTouchIDState:(long long)touchIDState forDSID:(nullable NSNumber *)dsID replyBlock:(nonnull void (^)(BOOL, NSError * _Nullable))block; // Unverified dsID type / block parameter types - (void)shouldSendGUIDWithRequestForURL:(nullable NSURL *)url withReplyBlock:(nonnull void (^)(BOOL))block; // Unverified url type / block parameter types - (void)signInWithContext:(nullable ISAuthenticationContext *)context replyBlock:(nonnull void (^)(BOOL, ISStoreAccount * _Nullable, NSError * _Nullable))block NS_DEPRECATED_MAC(10_9, 10_12); // Unverified - (void)signOut; - (void)storeFrontWithReplyBlock:(nonnull void (^)(NSString * _Nonnull))block; // Unverified block parameter types - (void)updateTouchIDSettingsForDSID:(nullable NSNumber *)dsID replyBlock:(nonnull void (^)(BOOL, NSError * _Nullable))block; // Unverified dsID type / block parameter types - (void)urlIsTrustedByURLBag:(nullable NSURL *)urlBag dsID:(nullable NSNumber *)dsID withReplyBlock:(nonnull void (^)(BOOL))block NS_AVAILABLE_MAC(13); // Unverified urlBag type / dsID type / block parameter types - (void)urlIsTrustedByURLBag:(nullable NSURL *)urlBag withReplyBlock:(nonnull void (^)(BOOL))block; // Unverified urlBag type / block parameter types - (void)valueForURLBagKey:(nullable NSString *)bagKey dsID:(nullable NSNumber *)dsID withReplyBlock:(nonnull void (^)(NSURL * _Nullable))block NS_AVAILABLE_MAC(13); // Unverified bagKey type / dsID type / block parameter types - (void)valueForURLBagKey:(nullable NSString *)bagKey withReplyBlock:(nonnull void (^)(NSURL * _Nullable))block; // Unverified bagKey type / block parameter types @optional @end ================================================ FILE: Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/StoreFoundation/ISServiceProxy.h ================================================ // // Generated by https://github.com/blacktop/ipsw (Version: 3.1.626, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26, SDK: 26, Tool: ld (1220.3) // - LC_SOURCE_VERSION: 716.0.8.0.0 // @interface ISServiceProxy : NSObject + (nonnull instancetype)genericSharedProxy; + (void)initialize; @property (readonly, nonatomic, nonnull) id accountService; @property (readonly, nonatomic, nonnull) id assetService; @property (readonly, nonatomic, nonnull) id downloadService; @property (readonly, nonatomic, weak, nullable) id exportedObject; @property (readonly, nonatomic, nullable) Protocol *exportedProtocol; @property (readonly, nonatomic, nonnull) id inAppService NS_DEPRECATED_MAC(10_9, 12); @property (retain, nonatomic, nullable) ISStoreClient *storeClient; @property (readonly, nonatomic, nonnull) id transactionService; @property (readonly, nonatomic, nonnull) id uiService; - (void)accountServiceSynchronousBlock:(nonnull UnknownBlock)block; - (nonnull id )accountServiceWithErrorHandler:(nullable UnknownBlock)handler; - (void)assetServiceSynchronousBlock:(nonnull UnknownBlock)block; - (nonnull id )assetServiceWithErrorHandler:(nullable UnknownBlock)handler; - (void)connectionWasInterrupted; - (nonnull NSXPCConnection *)connectionWithServiceName:(nonnull NSString *)serviceName protocol:(nonnull Protocol *)protocol isMachService:(BOOL)isMachService; - (void)downloadServiceSynchronousBlock:(nonnull UnknownBlock)block; - (nonnull id )downloadServiceWithErrorHandler:(nullable UnknownBlock)handler; - (void)inAppServiceSynchronousBlock:(nonnull UnknownBlock)block NS_DEPRECATED_MAC(10_9, 12); - (nonnull id )inAppServiceWithErrorHandler:(nullable UnknownBlock)handler NS_DEPRECATED_MAC(10_9, 12); - (nonnull instancetype)initWithStoreClient:(nullable ISStoreClient *)client; // Unverified client type - (nonnull id)objectProxyForServiceName:(nonnull NSString *)serviceName protocol:(nonnull id)protocol interfaceClassName:(nullable NSString *)interfaceClassName isMachService:(BOOL)isMachService errorHandler:(nullable UnknownBlock)handler; - (void)performSynchronousBlock:(nonnull UnknownBlock)block withServiceName:(nonnull NSString *)serviceName protocol:(nonnull Protocol *)protocol isMachService:(BOOL)isMachService interfaceClassName:(nullable NSString *)interfaceClassName; - (void)registerForInterrptionNotification; - (void)transactionServiceSynchronousBlock:(nonnull UnknownBlock)block; - (nonnull id )transactionServiceWithErrorHandler:(nullable UnknownBlock)handler; - (void)uiServiceSynchronousBlock:(nonnull UnknownBlock)block; - (nonnull id )uiServiceWithErrorHandler:(nullable UnknownBlock)handler; @end ================================================ FILE: Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/StoreFoundation/ISStoreAccount.h ================================================ // // Generated by https://github.com/blacktop/ipsw (Version: 3.1.626, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26, SDK: 26, Tool: ld (1220.3) // - LC_SOURCE_VERSION: 716.0.8.0.0 // @interface ISStoreAccount : NSObject { NSTimer *_tokenInvalidTimer; } + (nullable NSNumber *)dsidFromPlistValue:(nullable id)value; + (nonnull NSDictionary *)migratePersistedStoreDictionary:(nullable NSDictionary *)dictionary; + (BOOL)supportsSecureCoding; @property long long URLBagType; @property (readonly, getter=isAuthenticated) BOOL authenticated; @property (copy, nullable) NSString *creditString; @property (copy, nullable) NSNumber *dsID; @property (copy, nullable) NSString *identifier; @property BOOL isManagedStudent; @property BOOL isSignedIn; @property long long kind; @property (copy, nullable) NSString *password; @property (readonly, getter=isPrimary) BOOL primary; @property (retain, nullable) NSString *storeFront; @property (copy, nullable) NSString *token; @property (retain, nullable) NSTimer *tokenExpirationTimer; @property (retain, nullable) NSDate *tokenIssuedDate; @property long long touchIDState; - (nonnull NSString *)description; - (void)encodeWithCoder:(nullable NSCoder *)coder; - (long long)getTouchIDState; - (BOOL)hasValidStrongToken; - (nonnull instancetype)initWithCoder:(nullable NSCoder *)coder; - (nonnull instancetype)initWithPersistedStoreDictionary:(nullable NSDictionary *)dictionary; - (void)mergeValuesFromAuthenticationResponse:(nullable ISAuthenticationResponse *)response; - (nonnull NSDictionary *)persistedStoreDictionary; - (void)resetTouchIDState; - (double)strongTokenValidForSecond; @end ================================================ FILE: Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/StoreFoundation/SSDownload.h ================================================ // // Generated by https://github.com/blacktop/ipsw (Version: 3.1.626, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26, SDK: 26, Tool: ld (1220.3) // - LC_SOURCE_VERSION: 716.0.8.0.0 // @interface SSDownload : NSObject { BOOL _needsPreInstallValidation; } + (BOOL)supportsSecureCoding; @property (copy, nullable) NSNumber *accountDSID; @property (copy, nonatomic, nullable) NSArray *assets; // Unverified generic type @property (copy, nullable) NSString *cancelURLString; @property (copy, nullable) NSString *customDownloadPath; @property BOOL didAutoUpdate; @property unsigned long long downloadType; @property BOOL installAfterLogout; @property (copy, nullable) NSString *installPath; @property BOOL isInServerQueue; @property (copy, nonatomic, nullable) SSDownloadMetadata *metadata; @property BOOL needsDisplayInDock; @property (copy, nullable) NSURL *relaunchAppWithBundleURL; @property BOOL skipAssetDownloadIfNotAlreadyOnDisk; @property BOOL skipInstallPhase; @property (retain, nonatomic, nullable) SSDownloadStatus *status; - (void)cancel; - (void)cancelWithPrompt:(BOOL)prompt; - (void)cancelWithPrompt:(BOOL)prompt storeClient:(nullable ISStoreClient *)client; - (void)cancelWithStoreClient:(nullable ISStoreClient *)client; - (void)encodeWithCoder:(nullable NSCoder *)coder; - (nonnull instancetype)init; - (nonnull instancetype)initWithAssets:(nullable NSArray *)assets metadata:(nullable SSDownloadMetadata *)metadata; // Unverified assets type / metadata type - (nonnull instancetype)initWithCoder:(nullable NSCoder *)coder; - (BOOL)isEqual:(nullable id)object; - (void)pause; - (void)pauseWithStoreClient:(nullable ISStoreClient *)client; - (nullable SSDownloadAsset *)primaryAsset; - (void)resume; - (void)resumeWithStoreClient:(nullable ISStoreClient *)client; - (void)setUseUniqueDownloadFolder:(BOOL)useUniqueDownloadFolder; @end ================================================ FILE: Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/StoreFoundation/SSDownloadMetadata.h ================================================ // // Generated by https://github.com/blacktop/ipsw (Version: 3.1.626, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26, SDK: 26, Tool: ld (1220.3) // - LC_SOURCE_VERSION: 716.0.8.0.0 // @interface SSDownloadMetadata : NSObject { NSLock *_lock; } + (BOOL)supportsSecureCoding; @property (readonly, nonnull) NSNumber *ageRestriction; @property BOOL animationExpected; @property (retain, nullable) NSString *appleID; @property (readonly, nonnull) NSString *applicationIdentifier; @property BOOL artworkIsPrerendered; @property (readonly, nonnull) NSArray *assets; // Unverified generic type @property (readonly, nullable) NSString *bundleDisplayName; @property (retain, nullable) NSString *bundleIdentifier; @property (readonly, nullable) NSString *bundleShortVersionString; @property (retain, nullable) NSString *bundleVersion; @property (retain, nullable) NSString *buyParameters; @property (readonly, nullable) NSNumber *collectionID NS_AVAILABLE_MAC(13); @property (retain, nullable) NSString *collectionName; @property (retain, nullable) NSDictionary *dictionary; @property (retain, nullable) NSString *downloadKey; @property (retain, nullable) NSNumber *durationInMilliseconds; @property (retain, nullable) NSData *epubRightsData; @property (readonly) BOOL extractionCanBeStreamed; @property (retain, nullable) NSString *fileExtension; @property (retain, nullable) NSString *genre; @property (readonly, nullable) NSNumber *iapContentSize; @property (readonly, nullable) NSString *iapContentVersion; @property (retain, nullable) NSString *iapInstallPath; @property (retain, nullable) NSData *ipaInstallBookmarkData NS_AVAILABLE_MAC(14); @property (retain, nullable) NSString *ipaInstallPath; @property (readonly) BOOL isExplicitContents; @property BOOL isMDMProvided; @property unsigned long long itemIdentifier; @property (retain, nullable) NSString *kind; @property (retain, nullable) NSString *managedAppUUIDString NS_AVAILABLE_MAC(13); @property (readonly) BOOL needsSoftwareInstallOperation; @property (retain, nullable) NSURL *preflightPackageURL; @property (retain, nullable) NSString *productType; @property (readonly, nullable) NSString *purchaseDate; @property (getter=isRental) BOOL rental; @property (readonly, getter=isSample) BOOL sample; @property (retain, nullable) NSArray *> *sinfs; @property (readonly, nullable) NSString *sortArtist NS_AVAILABLE_MAC(13); @property (readonly, nullable) NSString *sortName NS_AVAILABLE_MAC(13); @property (retain, nullable) NSString *subtitle; @property (retain, nullable) NSURL *thumbnailImageURL; @property (retain, nullable) NSString *title; @property (retain, nullable) NSString *transactionIdentifier; @property (readonly, nullable) NSNumber *uncompressedSize; @property (retain, nullable) NSNumber *version NS_AVAILABLE_MAC(13); - (nullable id)_valueForFirstAvailableKey:(nullable id)key; - (nonnull instancetype)copyWithZone:(nullable struct _NSZone *)zone; - (nullable NSDictionary *)deltaPackages; // Unverified return type - (void)encodeWithCoder:(nullable NSCoder *)coder; - (nonnull instancetype)init; - (nonnull instancetype)initWithCoder:(nullable NSCoder *)coder; - (nonnull instancetype)initWithDictionary:(nullable NSDictionary *)dictionary; - (nonnull instancetype)initWithKind:(nullable NSString *)kind; - (nullable id)localServerInfo; - (void)setExtractionCanBeStreamed:(BOOL)extractionCanBeStreamed NS_DEPRECATED_MAC(10_9, 12); - (void)setUncompressedSize:(nullable NSNumber *)uncompressedSize NS_DEPRECATED_MAC(10_9, 12); - (void)setValue:(nullable id)value forMetadataKey:(nonnull NSString *)key; // Unverified key type @end ================================================ FILE: Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/StoreFoundation/SSDownloadPhase.h ================================================ // // Generated by https://github.com/blacktop/ipsw (Version: 3.1.626, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26, SDK: 26, Tool: ld (1220.3) // - LC_SOURCE_VERSION: 716.0.8.0.0 // @interface SSDownloadPhase : NSObject + (BOOL)supportsSecureCoding; @property (readonly) double estimatedSecondsRemaining; @property (readonly, nullable) SSOperationProgress *operationProgress; @property (readonly) long long phaseType; @property (readonly) float progressChangeRate; @property (readonly) long long progressUnits; @property (readonly) long long progressValue; @property (readonly) long long totalProgressValue; - (nonnull instancetype)copyWithZone:(nullable struct _NSZone *)zone; - (void)encodeWithCoder:(nullable NSCoder *)coder; - (nonnull instancetype)init; - (nonnull instancetype)initWithCoder:(nullable NSCoder *)coder; // Unverified coder type - (nonnull instancetype)initWithOperationProgress:(nullable SSOperationProgress *)progress; // Unverified progress type @end ================================================ FILE: Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/StoreFoundation/SSDownloadStatus.h ================================================ // // Generated by https://github.com/blacktop/ipsw (Version: 3.1.626, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26, SDK: 26, Tool: ld (1220.3) // - LC_SOURCE_VERSION: 716.0.8.0.0 // @interface SSDownloadStatus : NSObject + (BOOL)supportsSecureCoding; @property (readonly, nonatomic, nullable) SSDownloadPhase *activePhase; @property (nonatomic, getter=isCancelled) BOOL cancelled; @property (retain, nonatomic, nullable) NSError *error; @property (nonatomic, getter=isFailed) BOOL failed; @property (readonly, nonatomic, getter=isPausable) BOOL pausable; @property (nonatomic, getter=isPaused) BOOL paused; @property (readonly, nonatomic) float percentComplete; @property (readonly, nonatomic) float phasePercentComplete; @property (readonly, nonatomic) long long phaseTimeRemaining; @property BOOL waiting; - (nonnull instancetype)copyWithZone:(nullable struct _NSZone *)zone; - (void)encodeWithCoder:(nullable NSCoder *)coder; - (nonnull instancetype)initWithCoder:(nullable NSCoder *)coder; - (void)setOperationProgress:(nullable SSOperationProgress *)progress; // Unverified progress type @end ================================================ FILE: Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/StoreFoundation/SSPurchase.h ================================================ // // Generated by https://github.com/blacktop/ipsw (Version: 3.1.626, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26, SDK: 26, Tool: ld (1220.3) // - LC_SOURCE_VERSION: 716.0.8.0.0 // @interface SSPurchase : NSObject + (nonnull instancetype)purchaseWithBuyParameters:(nullable NSString *)buyParameters; // Unverified buyParameters type + (nonnull NSArray *> *)purchasesGroupedByAccountIdentifierWithPurchases:(nullable NSArray *)purchases; + (BOOL)supportsSecureCoding; @property (retain, nonatomic, nullable) NSNumber *accountIdentifier; @property (retain, nonatomic, nullable) NSString *appleID; @property (copy, nullable) UnknownBlock authFallbackHandler; // Unverified value type @property (copy, nonatomic, nullable) NSString *buyParameters; @property BOOL checkPreflightAterPurchase; @property (copy, nonatomic, nullable) SSDownloadMetadata *downloadMetadata; @property (retain, nullable) NSDictionary *dsidLessOptions; @property BOOL isCancelled; @property BOOL isDSIDLessPurchase; @property BOOL isRecoveryPurchase; @property BOOL isRedownload; @property BOOL isUpdate; @property BOOL isVPP; @property unsigned long long itemIdentifier; @property (retain, nonatomic, nullable) NSString *managedAppUUIDString NS_AVAILABLE_MAC(13); @property (readonly) BOOL needsAuthentication; @property (retain, nonatomic, nullable) NSString *parentalControls; @property (weak, nullable) ISOperation *purchaseOperation; @property (nonatomic) long long purchaseType; @property (retain, nonatomic, nullable) NSData *receiptData; @property (copy, nullable) NSDictionary *responseDialog; @property BOOL shouldBeInstalledAfterLogout; @property (readonly, nonatomic, nullable) NSString *sortableAccountIdentifier; @property (readonly, nonatomic, nonnull) NSString *uniqueIdentifier; - (nullable NSString *)_sortableAccountIdentifier; - (nonnull instancetype)copyWithZone:(nullable struct _NSZone *)zone; - (nonnull NSString *)description; - (void)encodeWithCoder:(nullable NSCoder *)coder; - (nonnull instancetype)initWithCoder:(nullable NSCoder *)coder; - (nonnull NSNumber *)productID; - (BOOL)purchaseDSIDMatchesPrimaryAccount; @end ================================================ FILE: Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/StoreFoundation/SSPurchaseResponse.h ================================================ // // Generated by https://github.com/blacktop/ipsw (Version: 3.1.626, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26, SDK: 26, Tool: ld (1220.3) // - LC_SOURCE_VERSION: 716.0.8.0.0 // @interface SSPurchaseResponse : NSObject { NSDictionary *_rawResponse; } + (BOOL)supportsSecureCoding; @property (retain, nullable) NSArray *downloads; @property (retain, nullable) NSDictionary *metrics; - (nonnull NSMutableArray *)_newDownloadsFromItems:(nullable NSArray *)items withDSID:(nullable NSNumber *)dsID; // Unverified items element generic types / dsID type - (void)encodeWithCoder:(nullable NSCoder *)coder; - (nonnull instancetype)initWithCoder:(nullable NSCoder *)coder; - (nonnull instancetype)initWithDictionary:(nullable NSDictionary *)dictionary userIdentifier:(nullable NSString *)userIdentifier; // Unverified dictionary generic types / userIdentifier type @end ================================================ FILE: Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/StoreFoundation/StoreFoundation.h ================================================ // // Generated by https://github.com/blacktop/ipsw (Version: 3.1.626, BuildCommit: Homebrew) // // - LC_BUILD_VERSION: Platform: macOS, MinOS: 26, SDK: 26, Tool: ld (1220.3) // - LC_SOURCE_VERSION: 716.0.8.0.0 // @import Foundation; @class ISAuthenticationContext, ISAuthenticationResponse, ISOperation, ISStoreClient, SSDownloadAsset, SSOperationProgress; @protocol ISAccountStoreObserver, ISAssetService, ISDownloadService, ISInAppService, ISServiceRemoteObject, ISTransactionService, ISUIService, ISURLBagObserver; typedef void (^UnknownBlock)(); #import "ISStoreAccount.h" #import "ISAccountService-Protocol.h" #import "ISServiceProxy.h" #import "SSDownloadMetadata.h" #import "SSDownloadPhase.h" #import "SSDownloadStatus.h" #import "SSDownload.h" #import "SSPurchase.h" #import "SSPurchaseResponse.h" ================================================ FILE: Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/StoreFoundation/module.modulemap ================================================ module StoreFoundation [system] [no_undeclared_includes] { requires macos, objc use Foundation link framework "StoreFoundation" umbrella header "StoreFoundation.h" export * } ================================================ FILE: Pearcleaner/Logic/AppsUpdater/SSPurchase+Extension.swift ================================================ // // SSPurchase+Extension.swift // Pearcleaner // // Created by Alin Lupascu on 10/13/25. // Based on mas-cli implementation // import Foundation import StoreFoundation typealias ADAMID = UInt64 extension SSPurchase { convenience init(adamID: ADAMID, purchasing: Bool, kind: String = "software") async { self.init( buyParameters: """ productType=C&price=0&pg=default&appExtVrsId=0&pricingParameters=\ \(purchasing ? "STDQ&macappinstalledconfirmed=1" : "STDRDL")&salableAdamId=\(adamID) """ ) // Possibly unnecessary… isRedownload = !purchasing itemIdentifier = adamID let downloadMetadata = SSDownloadMetadata(kind: kind) downloadMetadata.itemIdentifier = adamID self.downloadMetadata = downloadMetadata // Try to get Apple account info (may not be needed on macOS 12+) do { if let account = try await getAppleAccount() { accountIdentifier = NSNumber(value: account.dsID) appleID = account.emailAddress } } catch { // Do nothing - not required on modern macOS } } private func getAppleAccount() async throws -> (dsID: UInt64, emailAddress: String)? { // This is optional - macOS 12+ doesn't require account info for redownloads return nil } } ================================================ FILE: Pearcleaner/Logic/AppsUpdater/SparkleUpdateChecker.swift ================================================ // // SparkleUpdateChecker.swift // Pearcleaner // // Created by Alin Lupascu on 10/28/25. // // Simplified Sparkle update checker using SPUUpdater directly // import Foundation import Sparkle /// Result of Sparkle update check - makes Sparkle's decision explicit private enum SparkleUpdateResult { /// Sparkle found a valid update (always trust this) case updateFound( appcastItem: SUAppcastItem, state: SPUUserUpdateState ) /// Sparkle says no update needed case noUpdate( reason: SPUNoUpdateFoundReason, latestItem: SUAppcastItem? // May be nil if feed is empty ) /// Sparkle encountered an error case error(Error) } class SparkleUpdateChecker { fileprivate static let logger = UpdaterDebugLogger.shared /// Cached current macOS version (computed once per session) private static let currentMacOSVersion: Version = { let osVersion = ProcessInfo.processInfo.operatingSystemVersion let versionString = "\(osVersion.majorVersion).\(osVersion.minorVersion).\(osVersion.patchVersion)" return Version(versionNumber: versionString, buildNumber: nil) }() /// Check for Sparkle updates using SPUUpdater directly /// Only checks apps with SUFeedURL in Info.plist (no binary scanning) static func checkForUpdates(apps: [AppInfo], includePreReleases: Bool) async -> [UpdateableApp] { logger.log(.sparkle, "Starting Sparkle update check for \(apps.count) apps (includePreReleases: \(includePreReleases))") await GlobalConsoleManager.shared.appendOutput("Checking for Sparkle updates (\(apps.count) apps)...\n", source: CurrentPage.updater.title) // Filter apps that have Sparkle feed URLs var sparkleApps: [(appInfo: AppInfo, bundle: Bundle, feedURL: URL)] = [] for appInfo in apps { guard let bundle = Bundle(url: appInfo.path) else { continue } guard let feedURL = Self.feedURL(from: bundle) else { continue } logger.log(.sparkle, "Checking: \(appInfo.appName) - \(feedURL.absoluteString)") sparkleApps.append((appInfo, bundle, feedURL)) } guard !sparkleApps.isEmpty else { logger.log(.sparkle, "No Sparkle apps with valid feed URLs found") return [] } // Check apps in batches to prevent system resource exhaustion // Batch size adapts to CPU core count (min: 5, max: 50) // Lower minimum helps Intel Macs complete scans faster while still being safe var updates: [UpdateableApp] = [] let batches = createOptimalChunks(from: sparkleApps, minChunkSize: 5, maxChunkSize: 50) // Process each batch concurrently for batch in batches { // Check for cancellation between batches if Task.isCancelled { break } let batchResults = await withTaskGroup(of: UpdateableApp?.self) { group in for (appInfo, bundle, feedURL) in batch { group.addTask { await Self.checkSingleApp(appInfo: appInfo, bundle: bundle, feedURL: feedURL, includePreReleases: includePreReleases) } } // Collect non-nil results from this batch var batchUpdates: [UpdateableApp] = [] for await update in group { if let update = update { batchUpdates.append(update) } } return batchUpdates } updates.append(contentsOf: batchResults) } logger.log(.sparkle, "Found \(updates.count) Sparkle updates available") await GlobalConsoleManager.shared.appendOutput("Found \(updates.count) Sparkle update(s)\n", source: CurrentPage.updater.title) return updates } /// Check a single app for updates using SPUUpdater private static func checkSingleApp(appInfo: AppInfo, bundle: Bundle, feedURL: URL, includePreReleases: Bool) async -> UpdateableApp? { // Directly proceed with Sparkle check (no pre-flight URL check) // Users can hide slow/problematic apps using the eye button to prevent future checks let result = await withCheckedContinuation { continuation in DispatchQueue.main.async { let operation = SparkleCheckerOperation( appInfo: appInfo, bundle: bundle, feedURL: feedURL, includePreReleases: includePreReleases, currentMacOSVersion: Self.currentMacOSVersion ) { result in continuation.resume(returning: result) } operation.start() } } return result } /// Get Sparkle feed URL from app bundle /// Checks SUFeedURL in Info.plist, falls back to DevMate if framework is present static func feedURL(from bundle: Bundle) -> URL? { guard let information = bundle.infoDictionary else { return nil } // 1. Check SUFeedURL in Info.plist (standard Sparkle configuration) if let urlString = information["SUFeedURL"] as? String, let feedURL = URL(string: urlString.unquoted) { return feedURL } // 2. DevMate framework fallback (older apps) guard let bundleIdentifier = bundle.bundleIdentifier else { return nil } let frameworksURL = bundle.bundleURL.appendingPathComponent("Contents/Frameworks") let frameworks = try? FileManager.default.contentsOfDirectory(atPath: frameworksURL.path) if frameworks?.contains(where: { $0.contains("DevMateKit") }) ?? false { // DevMate apps use https://updates.devmate.com/{bundleIdentifier}.xml return URL(string: "https://updates.devmate.com")? .appendingPathComponent(bundleIdentifier) .appendingPathExtension("xml") } return nil } } // MARK: - SPUUpdater Operation /// Manages a single SPUUpdater instance to check for updates private class SparkleCheckerOperation: NSObject, SPUUserDriver, SPUUpdaterDelegate { private let appInfo: AppInfo private let bundle: Bundle private let feedURL: URL private let includePreReleases: Bool private let currentMacOSVersion: Version private let completion: (UpdateableApp?) -> Void private var updater: SPUUpdater? // Cache for bestValidUpdate result (Sparkle calls it 3 times during check phase) private var cachedBestUpdate: SUAppcastItem? private var hasCachedResult = false // Track if we've cached anything (nil or item) private var bestUpdateCallCount = 0 init(appInfo: AppInfo, bundle: Bundle, feedURL: URL, includePreReleases: Bool, currentMacOSVersion: Version, completion: @escaping (UpdateableApp?) -> Void) { self.appInfo = appInfo self.bundle = bundle self.feedURL = feedURL self.includePreReleases = includePreReleases self.currentMacOSVersion = currentMacOSVersion self.completion = completion super.init() } func start() { // Log what we're passing to Sparkle for diagnostics SparkleUpdateChecker.logger.log(.sparkle, " 🔍 Creating SPUUpdater:") SparkleUpdateChecker.logger.log(.sparkle, " Bundle path: \(bundle.bundlePath)") SparkleUpdateChecker.logger.log(.sparkle, " CFBundleVersion: \(bundle.infoDictionary?["CFBundleVersion"] as? String ?? "nil")") SparkleUpdateChecker.logger.log(.sparkle, " CFBundleShortVersionString: \(bundle.infoDictionary?["CFBundleShortVersionString"] as? String ?? "nil")") SparkleUpdateChecker.logger.log(.sparkle, " Feed URL: \(feedURL.absoluteString)") SparkleUpdateChecker.logger.log(.sparkle, " Include pre-releases: \(includePreReleases)") // Create SPUUpdater with this operation as both user driver and delegate let updater = SPUUpdater(hostBundle: bundle, applicationBundle: bundle, userDriver: self, delegate: self) do { try updater.start() updater.checkForUpdates() self.updater = updater } catch { SparkleUpdateChecker.logger.log(.sparkle, " ❌ Failed to start updater: \(error.localizedDescription)") finish(with: nil) } } private func finish(with update: UpdateableApp?) { completion(update) updater = nil } private func createUpdate(from appcastItem: SUAppcastItem, includePreReleases: Bool) -> UpdateableApp? { // Get versions from appcast item for display let availableVersionString = appcastItem.displayVersionString let buildVersionString = appcastItem.versionString // Note: Item was already validated by bestValidUpdate delegate method // Only need to filter pre-releases and extract metadata here SparkleUpdateChecker.logger.log(.sparkle, " Processing validated update: \(availableVersionString) (build: \(buildVersionString))") // Check if this is a pre-release var isPreRelease = false // Method 1: Sparkle 2.0+ channels (modern apps) if let channel = appcastItem.channel, channel.lowercased() != "release" { if !includePreReleases { SparkleUpdateChecker.logger.log(.sparkle, " Filtering: Pre-release channel '\(channel)' (toggle off)") return nil } isPreRelease = true SparkleUpdateChecker.logger.log(.sparkle, " Detected pre-release channel: \(channel)") } // Method 2: Version string analysis (legacy apps like Transmission) if isPreReleaseVersion(availableVersionString) { if !includePreReleases { SparkleUpdateChecker.logger.log(.sparkle, " Filtering: Pre-release version name '\(availableVersionString)' (toggle off)") return nil } isPreRelease = true SparkleUpdateChecker.logger.log(.sparkle, " Detected pre-release version name: \(availableVersionString)") } // Extract release notes var releaseTitle: String? var releaseDescription: String? var releaseNotesLink: String? var fetchedReleaseNotes: String? if let title = appcastItem.title { releaseTitle = title } if let description = appcastItem.itemDescription { releaseDescription = description } if let notesURL = appcastItem.releaseNotesURL ?? appcastItem.fullReleaseNotesURL { releaseNotesLink = notesURL.absoluteString // Note: Cannot fetch here synchronously - will be fetched on-demand when sidebar row appears fetchedReleaseNotes = nil } return UpdateableApp( appInfo: appInfo, availableVersion: availableVersionString, availableBuildNumber: buildVersionString, source: .sparkle, adamID: nil, appStoreURL: nil, status: .idle, progress: 0.0, isSelectedForUpdate: false, releaseTitle: releaseTitle, releaseDescription: releaseDescription, releaseNotesLink: releaseNotesLink, releaseDate: appcastItem.dateString, isPreRelease: isPreRelease, isIOSApp: false, foundInRegion: nil, fetchedReleaseNotes: fetchedReleaseNotes, appcastItem: appcastItem // Cache the validated appcast item ) } // MARK: - SPUUserDriver func show(_ request: SPUUpdatePermissionRequest, reply: @escaping (SUUpdatePermissionResponse) -> Void) { // Disable automatic update checks and system profiling reply(SUUpdatePermissionResponse(automaticUpdateChecks: false, sendSystemProfile: false)) } func showUpdateFound(with appcastItem: SUAppcastItem, state: SPUUserUpdateState, reply: @escaping (SPUUserUpdateChoice) -> Void) { let result = SparkleUpdateResult.updateFound(appcastItem: appcastItem, state: state) processResult(result, reply: reply, acknowledgement: nil) } func showUpdateNotFoundWithError(_ error: Error, acknowledgement: @escaping () -> Void) { let result = extractResultFromNoUpdateError(error) processResult(result, reply: nil, acknowledgement: acknowledgement) } func showUpdaterError(_ error: Error, acknowledgement: @escaping () -> Void) { let result = SparkleUpdateResult.error(error) processResult(result, reply: nil, acknowledgement: acknowledgement) } // MARK: - Helper Methods /// Extract structured result from Sparkle's "no update" error private func extractResultFromNoUpdateError(_ error: Error) -> SparkleUpdateResult { let nsError = error as NSError // Verify this is actually a "no update" error from Sparkle guard nsError.domain == SUSparkleErrorDomain, nsError.code == SUError.noUpdateError.rawValue else { // Some other error - treat as genuine error return .error(error) } // Extract latest appcast item (may be nil if feed is empty) let latestItem = nsError.userInfo[SPULatestAppcastItemFoundKey] as? SUAppcastItem // Extract Sparkle's reason for no update let reasonRawValue = (nsError.userInfo[SPUNoUpdateFoundReasonKey] as? NSNumber)?.int32Value ?? 0 let sparkleReason = SPUNoUpdateFoundReason(rawValue: reasonRawValue) ?? .unknown return .noUpdate(reason: sparkleReason, latestItem: latestItem) } /// Format SPUNoUpdateFoundReason as human-readable string private func formatNoUpdateReason(_ reason: SPUNoUpdateFoundReason) -> String { switch reason { case .onLatestVersion: return "Already on latest version" case .onNewerThanLatestVersion: return "Newer than latest (ahead of feed)" case .systemIsTooOld: return "System too old for update" case .systemIsTooNew: return "System too new for update" default: return "Unknown reason" } } /// Log comprehensive details about what Sparkle's callback told us private func logSparkleCallback(_ result: SparkleUpdateResult) { switch result { case .updateFound(let appcastItem, let state): SparkleUpdateChecker.logger.log(.sparkle, " 📥 CALLBACK: showUpdateFound") SparkleUpdateChecker.logger.log(.sparkle, " App: \(appInfo.appName)") SparkleUpdateChecker.logger.log(.sparkle, " Installed: \(appInfo.appVersion) (build: \(appInfo.appBuildNumber ?? "unknown"))") SparkleUpdateChecker.logger.log(.sparkle, " Available: \(appcastItem.displayVersionString) (build: \(appcastItem.versionString))") if let minOS = appcastItem.minimumSystemVersion { SparkleUpdateChecker.logger.log(.sparkle, " Min system version: \(minOS)") } if let channel = appcastItem.channel { SparkleUpdateChecker.logger.log(.sparkle, " Channel: \(channel)") } if let date = appcastItem.dateString { SparkleUpdateChecker.logger.log(.sparkle, " Release date: \(date)") } let stageDesc = state.stage == .notDownloaded ? "not downloaded" : state.stage == .downloaded ? "downloaded" : "installing" let initiatedDesc = state.userInitiated ? "user-initiated" : "automatic" SparkleUpdateChecker.logger.log(.sparkle, " State: \(stageDesc), \(initiatedDesc)") case .noUpdate(let reason, let latestItem): SparkleUpdateChecker.logger.log(.sparkle, " 📥 CALLBACK: showUpdateNotFoundWithError") SparkleUpdateChecker.logger.log(.sparkle, " App: \(appInfo.appName)") SparkleUpdateChecker.logger.log(.sparkle, " Installed: \(appInfo.appVersion)") if let item = latestItem { SparkleUpdateChecker.logger.log(.sparkle, " Latest: \(item.displayVersionString) (build: \(item.versionString))") if let title = item.title { SparkleUpdateChecker.logger.log(.sparkle, " Title: \(title)") } } else { SparkleUpdateChecker.logger.log(.sparkle, " Latest: (none in feed)") } let reasonDesc = formatNoUpdateReason(reason) SparkleUpdateChecker.logger.log(.sparkle, " Reason: \(reasonDesc)") case .error(let error): SparkleUpdateChecker.logger.log(.sparkle, " 📥 CALLBACK: showUpdaterError") SparkleUpdateChecker.logger.log(.sparkle, " App: \(appInfo.appName)") SparkleUpdateChecker.logger.log(.sparkle, " Error: \(error.localizedDescription)") let nsError = error as NSError SparkleUpdateChecker.logger.log(.sparkle, " Domain: \(nsError.domain), Code: \(nsError.code)") } } /// Process Sparkle's result with comprehensive logging and decision-making private func processResult( _ result: SparkleUpdateResult, reply: ((SPUUserUpdateChoice) -> Void)?, acknowledgement: (() -> Void)? ) { // Log what Sparkle told us (comprehensive callback details) logSparkleCallback(result) // Process based on result type switch result { case .updateFound(let appcastItem, _): // Trust Sparkle - it says update is available SparkleUpdateChecker.logger.log(.sparkle, " → Processing update found callback") // Apply our pre-release filtering if toggle is OFF let update = createUpdate(from: appcastItem, includePreReleases: includePreReleases) if let update = update { SparkleUpdateChecker.logger.log(.sparkle, " ✅ Showing update: \(update.appInfo.appName) \(appInfo.appVersion) → \(update.availableVersion ?? "unknown")") if update.isPreRelease { SparkleUpdateChecker.logger.log(.sparkle, " 🔵 Marked as pre-release") } } else { // Our filtering rejected it (pre-release filtered or version not newer) SparkleUpdateChecker.logger.log(.sparkle, " ⚠️ Update filtered out by Pearcleaner logic") } finish(with: update) case .noUpdate(let reason, let latestItem): // Sparkle says no update - trust it completely! let reasonDesc = formatNoUpdateReason(reason) SparkleUpdateChecker.logger.log(.sparkle, " → No update needed: \(reasonDesc)") if let item = latestItem { let latestVersion = item.displayVersionString let latestBuild = item.versionString SparkleUpdateChecker.logger.log(.sparkle, " Latest in feed: \(latestVersion) (build: \(latestBuild))") } else { SparkleUpdateChecker.logger.log(.sparkle, " Latest in feed: (none)") } finish(with: nil) acknowledgement?() case .error(let error): SparkleUpdateChecker.logger.log(.sparkle, " ❌ Sparkle error: \(error.localizedDescription)") finish(with: nil) acknowledgement?() } } func showUpdateInstalledAndRelaunched(_ relaunched: Bool, acknowledgement: @escaping () -> Void) { acknowledgement() finish(with: nil) } // MARK: - Ignored SPUUserDriver Methods func showUserInitiatedUpdateCheck(cancellation: @escaping () -> Void) {} func showUpdateReleaseNotes(with downloadData: SPUDownloadData) {} func showUpdateReleaseNotesFailedToDownloadWithError(_ error: Error) {} func showUpdateInFocus() {} func showDownloadInitiated(cancellation: @escaping () -> Void) {} func showDownloadDidReceiveExpectedContentLength(_ expectedContentLength: UInt64) {} func showDownloadDidReceiveData(ofLength length: UInt64) {} func showDownloadDidStartExtractingUpdate() {} func showExtractionReceivedProgress(_ progress: Double) {} func showReady(toInstallAndRelaunch reply: @escaping (SPUUserUpdateChoice) -> Void) {} func showInstallingUpdate(withApplicationTerminated applicationTerminated: Bool, retryTerminatingApplication: @escaping () -> Void) {} func showCanCheck(forUpdates canCheckForUpdates: Bool) {} func dismissUserInitiatedUpdateCheck() {} func showSendingTerminationSignal() {} func dismissUpdateInstallation() {} // MARK: - SPUUpdaterDelegate func feedURLString(for updater: SPUUpdater) -> String? { // Provide the feed URL to Sparkle return feedURL.absoluteString } func allowedChannels(for updater: SPUUpdater) -> Set { // If pre-releases are enabled, allow common pre-release channels // If disabled, return empty set (only default/stable channel) guard includePreReleases else { return [] } // Common pre-release channel names used by Sparkle apps return ["beta", "alpha", "nightly", "rc", "dev"] } func bestValidUpdate(in appcast: SUAppcast, for updater: SPUUpdater) -> SUAppcastItem? { bestUpdateCallCount += 1 // Sparkle calls this 3 times during check phase with slightly different filtered appcasts // Return cached result for calls 2 and 3 to avoid redundant work if hasCachedResult { SparkleUpdateChecker.logger.log(.sparkle, " 🔍 bestValidUpdate called (call #\(bestUpdateCallCount)) - returning cached result") if let cached = cachedBestUpdate { SparkleUpdateChecker.logger.log(.sparkle, " Cached: \(cached.displayVersionString) (build: \(cached.versionString))") } else { SparkleUpdateChecker.logger.log(.sparkle, " Cached: nil (no update available)") } return cachedBestUpdate } SparkleUpdateChecker.logger.log(.sparkle, " 🔍 bestValidUpdate called (call #\(bestUpdateCallCount)) - finding newest valid version") SparkleUpdateChecker.logger.log(.sparkle, " Total items in appcast: \(appcast.items.count)") // Get installed version info for comparison let installedDisplayVersion = appInfo.appVersion let installedBuild = appInfo.appBuildNumber SparkleUpdateChecker.logger.log(.sparkle, " Installed: \(installedDisplayVersion) (build: \(installedBuild ?? "nil"))") // Sort items by display version (newest first) using Version struct let sortedItems = appcast.items.sorted { item1, item2 in let ver1 = Version(versionNumber: item1.displayVersionString, buildNumber: nil) let ver2 = Version(versionNumber: item2.displayVersionString, buildNumber: nil) return ver1 > ver2 // Descending order (newest first) } SparkleUpdateChecker.logger.log(.sparkle, " Sorted items (newest → oldest):") for (index, item) in sortedItems.enumerated() { SparkleUpdateChecker.logger.log(.sparkle, " \(index + 1). \(item.displayVersionString) (build: \(item.versionString))") } // Filter items for OS compatibility and pre-release requirements SparkleUpdateChecker.logger.log(.sparkle, " ") SparkleUpdateChecker.logger.log(.sparkle, " Filtering for OS compatibility and pre-release settings...") for (index, item) in sortedItems.enumerated() { // Filter 1: Check OS compatibility (using cached OS version) if let minOS = item.minimumSystemVersion { let minOSVersion = Version(versionNumber: minOS, buildNumber: nil) if currentMacOSVersion < minOSVersion { SparkleUpdateChecker.logger.log(.sparkle, " ❌ Filtered #\(index + 1): Requires macOS \(minOS), current: \(currentMacOSVersion.versionNumber ?? "unknown")") continue } } // Filter 2: Check channel (pre-release toggle) if let channel = item.channel, channel.lowercased() != "release" { if !includePreReleases { SparkleUpdateChecker.logger.log(.sparkle, " ❌ Filtered #\(index + 1): Pre-release channel '\(channel)' (toggle off)") continue } else { SparkleUpdateChecker.logger.log(.sparkle, " ℹ️ Item #\(index + 1): Pre-release channel '\(channel)' (allowed)") } } // Filter 3: Check if version string indicates pre-release (legacy apps) if isPreReleaseVersion(item.displayVersionString) { if !includePreReleases { SparkleUpdateChecker.logger.log(.sparkle, " ❌ Filtered #\(index + 1): Pre-release version name '\(item.displayVersionString)' (toggle off)") continue } else { SparkleUpdateChecker.logger.log(.sparkle, " ℹ️ Item #\(index + 1): Pre-release version name '\(item.displayVersionString)' (allowed)") } } // Found the newest valid candidate - now check if it's newer than installed SparkleUpdateChecker.logger.log(.sparkle, " ") SparkleUpdateChecker.logger.log(.sparkle, " 🎯 Newest valid candidate: \(item.displayVersionString) (build: \(item.versionString))") SparkleUpdateChecker.logger.log(.sparkle, " Checking if newer than installed version...") // Dual-check strategy: Check BOTH display version AND build number let installedDisplayVer = Version(versionNumber: installedDisplayVersion, buildNumber: nil) let availableDisplayVer = Version(versionNumber: item.displayVersionString, buildNumber: nil) // Check #1: Display version semantic comparison let displayIsNewer = availableDisplayVer > installedDisplayVer SparkleUpdateChecker.logger.log(.sparkle, " Check #1 (Display): \(installedDisplayVersion) → \(item.displayVersionString) = \(displayIsNewer ? "✅ NEWER" : "❌ not newer")") // Check #2: Build number lexicographical comparison var buildIsNewer = false if let installedBuild = installedBuild { let comparison = item.versionString.compare(installedBuild, options: .numeric) buildIsNewer = (comparison == .orderedDescending) SparkleUpdateChecker.logger.log(.sparkle, " Check #2 (Build): \(installedBuild) → \(item.versionString) = \(buildIsNewer ? "✅ NEWER" : "❌ not newer")") } else { SparkleUpdateChecker.logger.log(.sparkle, " Check #2 (Build): Skipped (no installed build number)") } // Accept if EITHER check shows update if displayIsNewer || buildIsNewer { SparkleUpdateChecker.logger.log(.sparkle, " ") SparkleUpdateChecker.logger.log(.sparkle, " ✅ SELECTED: \(item.displayVersionString) (build: \(item.versionString))") SparkleUpdateChecker.logger.log(.sparkle, " Reason: \(displayIsNewer ? "Display version is newer" : "Build number is newer")") // Cache result for subsequent calls (Sparkle calls this 3 times) cachedBestUpdate = item hasCachedResult = true return item } else { SparkleUpdateChecker.logger.log(.sparkle, " ⚠️ Candidate is not newer than installed version") SparkleUpdateChecker.logger.log(.sparkle, " No update available") // Cache nil result for subsequent calls cachedBestUpdate = nil hasCachedResult = true return nil } } // No valid candidate found after filtering SparkleUpdateChecker.logger.log(.sparkle, " ") SparkleUpdateChecker.logger.log(.sparkle, " ❌ No valid update found (all items filtered out)") // Cache nil result for subsequent calls cachedBestUpdate = nil hasCachedResult = true return nil } } // MARK: - String Extension private extension String { /// Removes surrounding quotes and apostrophes from the string /// Handles malformed Info.plist entries like: "https://example.com" or 'https://example.com' var unquoted: String { return trimmingCharacters(in: CharacterSet(charactersIn: "'\"")) } } ================================================ FILE: Pearcleaner/Logic/AppsUpdater/SparkleUpdateDriver.swift ================================================ // // SparkleUpdateDriver.swift // Pearcleaner // // Custom SPUUserDriver for programmatically controlling Sparkle updates. // Allows Pearcleaner to download and install updates for third-party Sparkle apps directly. // import Foundation import Sparkle class SparkleUpdateDriver: NSObject, SPUUserDriver, SPUUpdaterDelegate, @unchecked Sendable { // MARK: - Properties private let appBundle: Bundle private let includePreReleases: Bool private let cachedAppcastItem: SUAppcastItem? // Pre-validated item from check phase private var updater: SPUUpdater? private let progressCallback: (Double, UpdateStatus) -> Void private let completionCallback: (Bool, Error?) -> Void private var downloadedBytes: Int64 = 0 private var totalBytes: Int64 = 0 private let logger = UpdaterDebugLogger.shared // MARK: - Initialization init(appInfo: AppInfo, includePreReleases: Bool, cachedAppcastItem: SUAppcastItem?, progressCallback: @escaping (Double, UpdateStatus) -> Void, completionCallback: @escaping (Bool, Error?) -> Void) { guard let bundle = Bundle(url: appInfo.path) else { fatalError("Could not create bundle for app at \(appInfo.path)") } self.appBundle = bundle self.includePreReleases = includePreReleases self.cachedAppcastItem = cachedAppcastItem self.progressCallback = progressCallback self.completionCallback = completionCallback super.init() // Debug: Log cached item status in driver if let cachedItem = cachedAppcastItem { logger.log(.sparkle, " 🔍 DEBUG: SparkleUpdateDriver received cached item: \(cachedItem.displayVersionString) (build: \(cachedItem.versionString))") } else { logger.log(.sparkle, " ⚠️ DEBUG: SparkleUpdateDriver received nil cached item") } } // MARK: - Public Methods func startUpdate() { logger.log(.sparkle, "━━━ Starting Sparkle update for \(appBundle.bundleIdentifier ?? "unknown")") logger.log(.sparkle, " App path: \(appBundle.bundlePath)") Task { @MainActor in GlobalConsoleManager.shared.appendOutput("Initializing Sparkle updater for \(appBundle.bundleURL.lastPathComponent)...\n", source: CurrentPage.updater.title) } // Check for public key if let publicKey = appBundle.object(forInfoDictionaryKey: "SUPublicEDKey") as? String { logger.log(.sparkle, " ✓ Found SUPublicEDKey: \(publicKey.prefix(20))...") } else { logger.log(.sparkle, " ⚠️ No SUPublicEDKey found") } updater = SPUUpdater( hostBundle: appBundle, applicationBundle: appBundle, userDriver: self, delegate: self ) do { try updater?.start() logger.log(.sparkle, " ✓ Sparkle updater started successfully") updater?.checkForUpdates() logger.log(.sparkle, " ✓ Triggered user-initiated update check (forces SPUUserDriver callbacks)") } catch { logger.log(.sparkle, " ❌ Failed to start updater: \(error.localizedDescription)") Task { @MainActor in GlobalConsoleManager.shared.appendOutput("✗ Failed to start Sparkle updater: \(error.localizedDescription)\n", source: CurrentPage.updater.title) } completionCallback(false, error) } } // MARK: - SPUUserDriver Protocol (Auto-approve installation, track progress) func show(_ request: SPUUpdatePermissionRequest, reply: @escaping (SUUpdatePermissionResponse) -> Void) { // Auto-approve without showing permission dialog reply(SUUpdatePermissionResponse(automaticUpdateChecks: true, sendSystemProfile: false)) } func showUpdateFound(with appcastItem: SUAppcastItem, state: SPUUserUpdateState, reply: @escaping (SPUUserUpdateChoice) -> Void) { logger.log(.sparkle, " ✓ Update found: \(appcastItem.displayVersionString) (build \(appcastItem.versionString))") if let fileURL = appcastItem.fileURL { logger.log(.sparkle, " Download URL: \(fileURL.absoluteString)") } logger.log(.sparkle, " Auto-approving installation...") Task { @MainActor in GlobalConsoleManager.shared.appendOutput("Found update: \(appcastItem.displayVersionString), starting download...\n", source: CurrentPage.updater.title) } // Auto-approve installation (no UI) progressCallback(0.0, .downloading) reply(.install) } func showDownloadDidReceiveExpectedContentLength(_ expectedContentLength: UInt64) { totalBytes = Int64(expectedContentLength) let sizeStr = ByteCountFormatter.string(fromByteCount: Int64(expectedContentLength), countStyle: .file) logger.log(.sparkle, " Starting download (\(sizeStr))...") Task { @MainActor in GlobalConsoleManager.shared.appendOutput("Downloading update (\(sizeStr))...\n", source: CurrentPage.updater.title) } } func showDownloadDidReceiveData(ofLength length: UInt64) { downloadedBytes += Int64(length) let progress = totalBytes > 0 ? Double(downloadedBytes) / Double(totalBytes) : 0.0 // Log at 25%, 50%, 75% milestones let percentage = Int(progress * 100) if percentage > 0 && percentage % 25 == 0 { let downloaded = ByteCountFormatter.string(fromByteCount: downloadedBytes, countStyle: .file) let total = ByteCountFormatter.string(fromByteCount: totalBytes, countStyle: .file) logger.log(.sparkle, " Download progress: \(percentage)% (\(downloaded) / \(total))") } // Download = 0-75% of total progress progressCallback(progress * 0.75, .downloading) } func showExtractionReceivedProgress(_ progress: Double) { let percentage = Int(progress * 100) if percentage > 0 && percentage % 25 == 0 { logger.log(.sparkle, " Extraction progress: \(percentage)%") } // Log extraction start (once at 0%) if percentage == 0 { Task { @MainActor in GlobalConsoleManager.shared.appendOutput("Extracting update...\n", source: CurrentPage.updater.title) } } // Extraction = 75-95% of total progress progressCallback(0.75 + (progress * 0.20), .extracting) } func showInstallingUpdate(withApplicationTerminated: Bool, retryTerminatingApplication: @escaping () -> Void) { if withApplicationTerminated { logger.log(.sparkle, " ✓ Target app terminated, installing update...") Task { @MainActor in GlobalConsoleManager.shared.appendOutput("Target app terminated, installing update...\n", source: CurrentPage.updater.title) } } else { logger.log(.sparkle, " Installing update (app will be terminated)...") Task { @MainActor in GlobalConsoleManager.shared.appendOutput("Installing update (app will be terminated)...\n", source: CurrentPage.updater.title) } } // Installing = 95-100% progressCallback(0.95, .installing) } func showUpdateInstalledAndRelaunched(_ relaunched: Bool, acknowledgement: @escaping () -> Void) { logger.log(.sparkle, " ✓✓✓ Update installed successfully!") if relaunched { logger.log(.sparkle, " App relaunched") } Task { @MainActor in GlobalConsoleManager.shared.appendOutput("✓ Sparkle update installed successfully\n", source: CurrentPage.updater.title) if relaunched { GlobalConsoleManager.shared.appendOutput("App relaunched\n", source: CurrentPage.updater.title) } } progressCallback(1.0, .completed) completionCallback(true, nil) acknowledgement() } func showUpdaterError(_ error: Error, acknowledgement: @escaping () -> Void) { logger.log(.sparkle, " ❌❌❌ Sparkle updater error:") logger.log(.sparkle, " \(error.localizedDescription)") if let nsError = error as NSError? { logger.log(.sparkle, " Domain: \(nsError.domain), Code: \(nsError.code)") if let underlyingError = nsError.userInfo[NSUnderlyingErrorKey] as? Error { logger.log(.sparkle, " Underlying: \(underlyingError.localizedDescription)") } } Task { @MainActor in GlobalConsoleManager.shared.appendOutput("✗ Sparkle updater error: \(error.localizedDescription)\n", source: CurrentPage.updater.title) } completionCallback(false, error) acknowledgement() } // MARK: - SPUUserDriver Protocol (Stubbed UI methods) func showUserInitiatedUpdateCheck(cancellation: @escaping () -> Void) { // No UI - stub } func dismissUserInitiatedUpdateCheck() { // No UI - stub } func showUpdateNotFoundWithError(_ error: Error, acknowledgement: @escaping () -> Void) { logger.log(.sparkle, " ℹ️ No update found: \(error.localizedDescription)") Task { @MainActor in GlobalConsoleManager.shared.appendOutput("No update available: \(error.localizedDescription)\n", source: CurrentPage.updater.title) } acknowledgement() } func showUpdateReleaseNotes(with downloadData: SPUDownloadData) { // No UI - stub } func showUpdateReleaseNotesFailedToDownloadWithError(_ error: Error) { // No UI - stub } func showUpdateInFocus() { // No UI - stub } func showDownloadInitiated(cancellation: @escaping () -> Void) { // No UI - stub } func showDownloadDidStartExtractingUpdate() { // No UI - stub } func showReady(toInstallAndRelaunch reply: @escaping (SPUUserUpdateChoice) -> Void) { // Auto-approve installation reply(.install) } func showSendingTerminationSignal() { // No UI - stub } func dismissUpdateInstallation() { // No UI - stub } func showCanCheck(forUpdates canCheckForUpdates: Bool) { // No UI - stub } // MARK: - SPUUpdaterDelegate Protocol func feedURLString(for updater: SPUUpdater) -> String? { // Provide DevMate fallback for apps without SUFeedURL in Info.plist // SPUUpdater automatically reads SUFeedURL from Info.plist first, then calls this delegate return SparkleUpdateChecker.feedURL(from: updater.hostBundle)?.absoluteString } func allowedChannels(for updater: SPUUpdater) -> Set { // If pre-releases are enabled, allow common pre-release channels // If disabled, return empty set (only default/stable channel) guard includePreReleases else { return [] } // Common pre-release channel names used by Sparkle apps return ["beta", "alpha", "nightly", "rc", "dev"] } func bestValidUpdate(in appcast: SUAppcast, for updater: SPUUpdater) -> SUAppcastItem? { // If we have a cached appcast item from the check phase, use it // This ensures consistent version selection between check and install if let cachedItem = cachedAppcastItem { logger.log(.sparkle, " ✅ Using cached appcast item: \(cachedItem.displayVersionString) (build: \(cachedItem.versionString))") logger.log(.sparkle, " Skipping re-validation - item was already validated during check phase") return cachedItem } // No cached item - shouldn't happen in normal flow, but fall back to nil // Sparkle will use its own bestValidUpdate logic logger.log(.sparkle, " ⚠️ No cached appcast item - using Sparkle's default validation") return nil } } ================================================ FILE: Pearcleaner/Logic/AppsUpdater/SparkleUpdateOperation.swift ================================================ // // SparkleUpdateOperation.swift // Pearcleaner // // Operation subclass that blocks until Sparkle update completes using DispatchSemaphore. // Prevents concurrent Sparkle updates from conflicting with each other. // import Foundation class SparkleUpdateOperation: Operation, @unchecked Sendable { let app: UpdateableApp let includePreReleases: Bool let progressCallback: (Double, UpdateStatus) -> Void let completionCallback: (Bool, Error?) -> Void private let semaphore = DispatchSemaphore(value: 0) var bundleIdentifier: String { app.appInfo.bundleIdentifier } init( app: UpdateableApp, includePreReleases: Bool, progressCallback: @escaping (Double, UpdateStatus) -> Void, completionCallback: @escaping (Bool, Error?) -> Void ) { self.app = app self.includePreReleases = includePreReleases self.progressCallback = progressCallback self.completionCallback = completionCallback super.init() } override func main() { guard !isCancelled else { return } // Debug: Log cached appcast item status if let cachedItem = app.appcastItem { UpdaterDebugLogger.shared.log(.sparkle, "🔍 DEBUG: Cached appcast item found: \(cachedItem.displayVersionString) (build: \(cachedItem.versionString))") } else { UpdaterDebugLogger.shared.log(.sparkle, "⚠️ DEBUG: No cached appcast item - app.appcastItem is nil!") } // Sparkle must be initialized and started on the main thread DispatchQueue.main.sync { let driver = SparkleUpdateDriver( appInfo: app.appInfo, includePreReleases: includePreReleases, cachedAppcastItem: app.appcastItem, // Pass cached item from check phase progressCallback: progressCallback, completionCallback: { [weak self] success, error in guard let self = self else { return } // Call the original completion callback self.completionCallback(success, error) // Signal the semaphore to unblock the operation self.semaphore.signal() } ) driver.startUpdate() } // Block here until the update completes (semaphore.signal() is called in completion callback) semaphore.wait() } } ================================================ FILE: Pearcleaner/Logic/AppsUpdater/UpdateManager.swift ================================================ // // UpdateManager.swift // Pearcleaner // // Created by Alin Lupascu on 10/13/25. // import Foundation import SwiftUI import AlinFoundation @MainActor class UpdateManager: ObservableObject { static let shared = UpdateManager() @Published var updatesBySource: [UpdateSource: [UpdateableApp]] = [:] @Published var hiddenUpdates: [UpdateableApp] = [] @Published var isScanning: Bool = false @Published var lastScanDate: Date? @Published var scanningSources: Set = [] @Published var currentScanTask: Task? // Batch update tracking @Published var isUpdatingAll: Bool = false @Published var totalAppsToUpdate: Int = 0 // Consolidated settings (2 Data properties total) @AppStorage("settings.updater.sources") private var sourcesData: Data = UpdaterSourcesSettings.defaultEncoded() @AppStorage("settings.updater.display") private var displayData: Data = UpdaterDisplaySettings.defaultEncoded() @AppStorage("settings.updater.debugLogging") private var debugLogging: Bool = true @AppStorage("settings.updater.hiddenAppsData") private var hiddenAppsData: Data = Data() @AppStorage("settings.updater.ignoredAppsData") private var ignoredAppsData: Data = Data() // Computed properties for convenient access to nested structs private var sources: UpdaterSourcesSettings { get { UpdaterSourcesSettings.decode(from: sourcesData) } set { sourcesData = newValue.encode() } } private var display: UpdaterDisplaySettings { get { UpdaterDisplaySettings.decode(from: displayData) } set { displayData = newValue.encode() } } // Backward-compatible convenience properties private var checkAppStore: Bool { sources.appStore.enabled } private var checkHomebrew: Bool { sources.homebrew.enabled } private var checkSparkle: Bool { sources.sparkle.enabled } private var showAutoUpdatesInHomebrew: Bool { sources.homebrew.showAutoUpdates } private var includeSparklePreReleases: Bool { sources.sparkle.includePreReleases } private var showUnsupported: Bool { display.showUnsupported } private var showCurrent: Bool { display.showCurrent } private var hasAutoScannedOnce = false private init() { // Migrate old settings to new consolidated format migrateSettingsIfNeeded() // Migrate old hiddenApps data to new ignoredApps format on first launch migrateHiddenAppsIfNeeded() // Subscribe to notification for automatic background scanning NotificationCenter.default.addObserver( self, selector: #selector(handleAllAppsFullyLoaded), name: NSNotification.Name("AllAppsFullyLoaded"), object: nil ) } /// Migrate old individual settings to new consolidated structs (one-time migration) private func migrateSettingsIfNeeded() { // Check if migration already happened by seeing if new settings exist if sourcesData.isEmpty { // Read old settings from UserDefaults let oldCheckAppStore = UserDefaults.standard.bool(forKey: "settings.updater.checkAppStore") let oldCheckHomebrew = UserDefaults.standard.bool(forKey: "settings.updater.checkHomebrew") let oldCheckSparkle = UserDefaults.standard.bool(forKey: "settings.updater.checkSparkle") let oldShowAutoUpdates = UserDefaults.standard.bool(forKey: "settings.updater.showAutoUpdatesInHomebrew") let oldIncludePreReleases = UserDefaults.standard.bool(forKey: "settings.updater.includeSparklePreReleases") // Check if any old settings exist (they default to false if never set) let hasOldSettings = UserDefaults.standard.object(forKey: "settings.updater.checkAppStore") != nil || UserDefaults.standard.object(forKey: "settings.updater.checkHomebrew") != nil || UserDefaults.standard.object(forKey: "settings.updater.checkSparkle") != nil if hasOldSettings { // Migrate to new format var newSources = UpdaterSourcesSettings() newSources.appStore.enabled = oldCheckAppStore newSources.homebrew.enabled = oldCheckHomebrew newSources.homebrew.showAutoUpdates = oldShowAutoUpdates newSources.sparkle.enabled = oldCheckSparkle newSources.sparkle.includePreReleases = oldIncludePreReleases sources = newSources } } if displayData.isEmpty { // Read old showUnsupported setting let oldShowUnsupported = UserDefaults.standard.bool(forKey: "settings.updater.showUnsupported") if UserDefaults.standard.object(forKey: "settings.updater.showUnsupported") != nil { var newDisplay = UpdaterDisplaySettings() newDisplay.showUnsupported = oldShowUnsupported display = newDisplay } } } @objc private func handleAllAppsFullyLoaded() { // Only run once per app session guard !hasAutoScannedOnce else { return } hasAutoScannedOnce = true Task { @MainActor in await scanIfNeeded() } } /// Public entry point for triggering scans. Prevents duplicate scans through centralized logic. /// - Parameters: /// - forceReload: If true, bypasses cache and forces a fresh scan /// - sources: Optional set of specific sources to scan. If nil, scans all enabled sources. func scanIfNeeded(forceReload: Bool = false, sources: Set? = nil) async { // Prevent duplicate scans guard !isScanning else { return } // Trigger scan if forcing reload OR specific sources requested if forceReload || sources != nil { await scanForUpdates(forceReload: forceReload, sources: sources) return } // Otherwise, only scan if no data exists yet guard lastScanDate == nil else { return } await scanForUpdates() } var hasUpdates: Bool { updatesBySource.values.contains { !$0.isEmpty } || !hiddenUpdates.isEmpty } var totalUpdateCount: Int { updatesBySource .filter { $0.key != .unsupported && $0.key != .current } .values .reduce(0) { $0 + $1.count } } /// Progress of batch update (0.0 to 1.0) var updateAllProgress: Double { guard totalAppsToUpdate > 0 else { return 0 } let remaining = updatesBySource .filter { $0.key != .unsupported && $0.key != .current } .values .flatMap { $0 } .filter { $0.isSelectedForUpdate } .count return Double(totalAppsToUpdate - remaining) / Double(totalAppsToUpdate) } /// Number of completed apps in batch update var completedAppsCount: Int { guard totalAppsToUpdate > 0 else { return 0 } let remaining = updatesBySource .filter { $0.key != .unsupported && $0.key != .current } .values .flatMap { $0 } .filter { $0.isSelectedForUpdate } .count return totalAppsToUpdate - remaining } /// Computed property for easy access to hidden apps mapping (bundleID -> source) private var hiddenApps: [String: UpdateSource] { get { guard let decoded = try? JSONDecoder().decode([String: String].self, from: hiddenAppsData) else { return [:] } // Convert String to UpdateSource return decoded.compactMapValues { UpdateSource(rawValue: $0) } } set { // Convert UpdateSource to String for storage let stringDict = newValue.mapValues { $0.rawValue } hiddenAppsData = (try? JSONEncoder().encode(stringDict)) ?? Data() } } /// Unified ignored apps storage: bundleID -> [source -> version?] /// nil version = permanently ignored, string version = skip until newer version private var ignoredApps: [String: [String: String?]] { get { guard let decoded = try? JSONDecoder().decode([String: [String: String?]].self, from: ignoredAppsData) else { return [:] } return decoded } set { ignoredAppsData = (try? JSONEncoder().encode(newValue)) ?? Data() } } /// Migrate old hiddenApps data to new ignoredApps format (one-time migration) private func migrateHiddenAppsIfNeeded() { // Only migrate if old data exists and new data is empty guard !hiddenAppsData.isEmpty, ignoredAppsData.isEmpty else { return } var migrated: [String: [String: String?]] = [:] for (bundleID, source) in hiddenApps { // Convert to new format with nil version (permanent ignore) migrated[bundleID] = [source.rawValue: nil] } ignoredApps = migrated // Keep old data for now in case user downgrades } /// Get the ignored version for a specific app and source /// - Parameter app: The app to check /// - Returns: nil if permanently ignored, version string if skipped, or nil if not ignored for this source func getIgnoredVersion(for app: UpdateableApp) -> String? { return ignoredApps[app.uniqueIdentifier]?[app.source.rawValue] ?? nil } /// Update the fetched release notes for a specific app /// - Parameters: /// - appId: The UUID of the app to update /// - content: The fetched release notes content func updateFetchedReleaseNotes(for appId: UUID, content: String) { // Find and update the app in updatesBySource for (source, apps) in updatesBySource { if let index = apps.firstIndex(where: { $0.id == appId }) { var updatedApp = apps[index] updatedApp.fetchedReleaseNotes = content updatesBySource[source]?[index] = updatedApp return } } } /// Hide an app permanently or skip a specific version /// - Parameters: /// - app: The app to ignore /// - skipVersion: Optional version to skip. If nil, app is permanently ignored. If provided, only that version is skipped. func hideApp(_ app: UpdateableApp, skipVersion: String? = nil) { // Add to new unified ignored apps storage var ignored = ignoredApps if ignored[app.uniqueIdentifier] == nil { ignored[app.uniqueIdentifier] = [:] } ignored[app.uniqueIdentifier]?[app.source.rawValue] = skipVersion ignoredApps = ignored // Also update old storage for backward compatibility if skipVersion == nil { var hidden = hiddenApps hidden[app.uniqueIdentifier] = app.source hiddenApps = hidden } // Immediately remove from visible lists for instant UI feedback updatesBySource[app.source]?.removeAll { $0.uniqueIdentifier == app.uniqueIdentifier } // Add to hidden list for sidebar display if !hiddenUpdates.contains(where: { $0.uniqueIdentifier == app.uniqueIdentifier }) { hiddenUpdates.append(app) } } /// Rescan a single app to get fresh update data func recheckUpdate(for app: UpdateableApp) async -> UpdateableApp? { // Get fresh AppInfo from sortedApps (handles case where app was updated externally) guard let freshAppInfo = AppState.shared.sortedApps.first(where: { $0.bundleIdentifier == app.uniqueIdentifier }) else { return nil // App no longer exists } // Call appropriate source-specific checker based on app.source switch app.source { case .homebrew: let results = await HomebrewUpdateChecker.checkForUpdates( apps: [freshAppInfo], includeFormulae: false, showAutoUpdatesInHomebrew: showAutoUpdatesInHomebrew ) return results.first case .appStore: let results = await AppStoreUpdateChecker.checkForUpdates(apps: [freshAppInfo]) return results.first case .sparkle: let results = await SparkleUpdateChecker.checkForUpdates( apps: [freshAppInfo], includePreReleases: includeSparklePreReleases ) return results.first case .unsupported: return nil // Can't check unsupported apps case .current: return nil // Already current, no update available } } /// Unhide an app (remove from hidden filter and restore to visible list if it has an update) func unhideApp(_ app: UpdateableApp) async { // Remove from new unified ignored apps storage var ignored = ignoredApps ignored[app.uniqueIdentifier]?.removeValue(forKey: app.source.rawValue) if ignored[app.uniqueIdentifier]?.isEmpty == true { ignored.removeValue(forKey: app.uniqueIdentifier) } ignoredApps = ignored // Also remove from old storage for backward compatibility var hidden = hiddenApps hidden.removeValue(forKey: app.uniqueIdentifier) hiddenApps = hidden // Immediately remove from hidden list for instant UI feedback hiddenUpdates.removeAll { $0.uniqueIdentifier == app.uniqueIdentifier } // Rescan the app to get fresh update data if let refreshedApp = await recheckUpdate(for: app) { // Add refreshed app to visible list if var apps = updatesBySource[app.source] { apps.append(refreshedApp) // Sort alphabetically after adding using sortKey extension apps.sort { $0.appInfo.appName.sortKey < $1.appInfo.appName.sortKey } updatesBySource[app.source] = apps } else { updatesBySource[app.source] = [refreshedApp] } } // If nil returned, no update available anymore - don't add to visible list } /// Toggle selection state for an app in the update queue func toggleAppSelection(_ app: UpdateableApp) { guard var apps = updatesBySource[app.source], let index = apps.firstIndex(where: { $0.id == app.id }) else { return } apps[index].isSelectedForUpdate.toggle() updatesBySource[app.source] = apps } func scanForUpdates(forceReload: Bool = false, sources: Set? = nil) async { // Double-check to prevent race condition where multiple scans pass the guard guard !isScanning else { return } isScanning = true defer { isScanning = false scanningSources.removeAll() // Always clear scanning state on exit } // Determine which sources to scan var sourcesToScan: Set if let sources = sources { // Selective scan - only scan specified sources sourcesToScan = sources // Only clear specified sources from updatesBySource (preserve others) for source in sources { updatesBySource[source] = nil } // Don't clear hiddenUpdates - will be rebuilt at end } else { // Full scan - scan all enabled sources (current behavior) sourcesToScan = [] if checkAppStore { sourcesToScan.insert(.appStore) } if checkHomebrew { sourcesToScan.insert(.homebrew) } if checkSparkle { sourcesToScan.insert(.sparkle) } // Clear all results updatesBySource = [:] hiddenUpdates = [] // Clear to prevent stale entries (will be rebuilt from persistent storage) } scanningSources = sourcesToScan // Only flush caches and reload apps if explicitly requested or debug mode enabled // This significantly improves performance for regular update checks if forceReload || debugLogging || AppState.shared.sortedApps.isEmpty { // Flush bundle caches (useful for testing with fake versions in debug mode) Pearcleaner.flushBundleCaches(for: AppState.shared.sortedApps) // Reload apps from disk to detect newly installed/uninstalled apps let folderPaths = await MainActor.run { FolderSettingsManager.shared.folderPaths } await loadAppsAsync(folderPaths: folderPaths, useStreaming: false) } // Check for cancellation after loading apps if Task.isCancelled { return } // Use apps from AppState (either freshly loaded or existing) let apps = AppState.shared.sortedApps // Filter out ignored apps BEFORE checking for updates // This prevents wasting time on HEAD requests and SPUUpdater calls for ignored apps let ignoredAppIds = Set(ignoredApps.keys) let visibleApps = apps.filter { !ignoredAppIds.contains($0.bundleIdentifier) } // Launch concurrent scans with progressive updates await withTaskGroup(of: (UpdateSource, [UpdateableApp]).self) { group in if sourcesToScan.contains(.homebrew) { group.addTask { let results = await HomebrewUpdateChecker.checkForUpdates(apps: visibleApps, includeFormulae: false, showAutoUpdatesInHomebrew: self.showAutoUpdatesInHomebrew) return (.homebrew, results) } } if sourcesToScan.contains(.appStore) { group.addTask { // Use pre-categorized flag (instant check vs expensive receipt verification) let appStoreApps = visibleApps.filter { $0.isAppStore } let results = await AppStoreUpdateChecker.checkForUpdates(apps: appStoreApps) return (.appStore, results) } } if sourcesToScan.contains(.sparkle) { group.addTask { // Show all apps with Sparkle, regardless of other update sources // This allows users to see version differences across App Store/Homebrew/Sparkle // and choose which source to update from let sparkleApps = visibleApps.filter { $0.hasSparkle } let results = await SparkleUpdateChecker.checkForUpdates(apps: sparkleApps, includePreReleases: self.includeSparklePreReleases) return (.sparkle, results) } } // Process results as they complete for await (source, apps) in group { // Check for cancellation between source results if Task.isCancelled { // Still process results with empty arrays to trigger cleanup for source in scanningSources { await processSourceResults(source: source, apps: []) } break } await processSourceResults(source: source, apps: apps) } } // Check for cancellation before final processing if Task.isCancelled { return } // Deduplicate: Remove Homebrew apps that also exist in Sparkle (when auto_updates=true and toggle is ON) // Rationale: If an app has both Homebrew cask and Sparkle framework with auto_updates=true, // prefer the developer's choice (built-in Sparkle updater) and avoid showing in both categories if showAutoUpdatesInHomebrew, let homebrewApps = updatesBySource[.homebrew], let sparkleApps = updatesBySource[.sparkle] { // Build set of Sparkle app paths for quick lookup let sparkleAppPaths = Set(sparkleApps.map { $0.appInfo.path }) // Filter out Homebrew apps that have both: // 1. auto_updates=true (developer chose built-in updater) // 2. Sparkle framework (exists in Sparkle category) let deduplicatedHomebrew = homebrewApps.filter { brewApp in guard let autoUpdates = brewApp.appInfo.autoUpdates, autoUpdates else { return true // Keep: no auto_updates flag } // Exclude if app also exists in Sparkle (prefer Sparkle) return !sparkleAppPaths.contains(brewApp.appInfo.path) } // Update with deduplicated list updatesBySource[.homebrew] = deduplicatedHomebrew } // Calculate unsupported apps (always calculate - it's instant, toggle only controls UI visibility) let unsupportedApps = apps.filter { app in // Not a web app (web apps update with browser) !app.webApp && // Not an App Store app !app.isAppStore && // Not a Homebrew cask/formula app.cask == nil && // Doesn't have Sparkle !app.hasSparkle }.map { app in // Create UpdateableApp with unsupported source UpdateableApp( appInfo: app, availableVersion: nil, // Can't check updates availableBuildNumber: nil, source: .unsupported, adamID: nil, appStoreURL: nil, status: .idle, progress: 0.0, isSelectedForUpdate: false, // Can't update unsupported apps releaseTitle: nil, releaseDescription: nil, releaseNotesLink: nil, releaseDate: nil, isPreRelease: false, isIOSApp: false, foundInRegion: nil, appcastItem: nil ) } await processSourceResults(source: .unsupported, apps: unsupportedApps) // Calculate current apps (supported but up-to-date - no updates available) let currentApps = apps.filter { app in // Not a web app !app.webApp && // Must be supported (App Store, Homebrew, or Sparkle) (app.isAppStore || app.cask != nil || app.hasSparkle) && // But doesn't have an update available in any of the update sources !updatesBySource.values.flatMap { $0 }.contains(where: { $0.appInfo.path == app.path }) }.map { app in // Create UpdateableApp with current source UpdateableApp( appInfo: app, availableVersion: app.appVersion, // Already up-to-date availableBuildNumber: nil, source: .current, adamID: nil, appStoreURL: nil, status: .idle, progress: 0.0, isSelectedForUpdate: false, // Already current, no update needed releaseTitle: nil, releaseDescription: nil, releaseNotesLink: nil, releaseDate: nil, isPreRelease: false, isIOSApp: false, foundInRegion: nil, fetchedReleaseNotes: nil, appcastItem: nil ) } await processSourceResults(source: .current, apps: currentApps) // Rebuild hidden apps list for display // This ensures ALL hidden apps appear in the sidebar, even those without updates await rebuildHiddenAppsList(allApps: apps) lastScanDate = Date() // Print formatted debug report to console after scan completes if debugLogging { printOS("\n" + UpdaterDebugLogger.shared.generateDebugReport()) } // Clear task reference on completion currentScanTask = nil } /// Rebuild hidden apps list from storage for display in sidebar /// This populates hiddenUpdates with ALL hidden apps (even those without updates) private func rebuildHiddenAppsList(allApps: [AppInfo]) async { let hidden = hiddenApps // For each hidden app in storage, create an UpdateableApp for display for (bundleID, source) in hidden { // Skip if already in hiddenUpdates (was found during update check) if hiddenUpdates.contains(where: { $0.uniqueIdentifier == bundleID }) { continue } // Find the app in sortedApps guard let appInfo = allApps.first(where: { $0.bundleIdentifier == bundleID }) else { // App no longer exists, remove from hidden storage var mutableHidden = hidden mutableHidden.removeValue(forKey: bundleID) hiddenApps = mutableHidden continue } // Create UpdateableApp without update info (just for display) let updateableApp = UpdateableApp( appInfo: appInfo, availableVersion: nil, availableBuildNumber: nil, source: source, adamID: nil, appStoreURL: nil, status: .idle, progress: 0.0, isSelectedForUpdate: false, releaseTitle: nil, releaseDescription: nil, releaseNotesLink: nil, releaseDate: nil, isPreRelease: false, isIOSApp: false, foundInRegion: nil, appcastItem: nil ) hiddenUpdates.append(updateableApp) } } private func processSourceResults(source: UpdateSource, apps: [UpdateableApp]) async { // Sort alphabetically let sortedApps = apps.sorted { $0.appInfo.appName.sortKey < $1.appInfo.appName.sortKey } // Filter ignored and version-skipped apps let ignored = ignoredApps let visible = sortedApps.filter { app in // Check if app is in ignored list guard let ignoredVersions = ignored[app.uniqueIdentifier], let ignoredVersion = ignoredVersions[source.rawValue] else { return true // Not ignored, show it } // If ignoredVersion is nil, permanently ignored if ignoredVersion == nil { return false } // If ignoredVersion matches availableVersion, skip this version if let availableVersion = app.availableVersion, availableVersion == ignoredVersion { return false } // Newer version available, show it return true } let hiddenAppsFromSource = sortedApps.filter { app in guard let ignoredVersions = ignored[app.uniqueIdentifier], let ignoredVersion = ignoredVersions[source.rawValue] else { return false } return ignoredVersion == nil || app.availableVersion == ignoredVersion } // Update results (set to empty array even if no visible results to indicate "completed") updatesBySource[source] = visible // Add hidden apps to hidden list for app in hiddenAppsFromSource { if !hiddenUpdates.contains(where: { $0.uniqueIdentifier == app.uniqueIdentifier }) { hiddenUpdates.append(app) } } // Mark source as no longer scanning scanningSources.remove(source) } /// Cancel the current scan operation func cancelScan() { isScanning = false // Immediately update UI state currentScanTask?.cancel() currentScanTask = nil scanningSources.removeAll() // Clear scanning state for all sources } /// Remove pre-release apps from a specific source without rescanning /// This is more efficient than rescanning when toggling off pre-releases func removePreReleaseApps(from source: UpdateSource) { guard var apps = updatesBySource[source] else { return } // Filter out pre-release apps apps = apps.filter { !$0.isPreRelease } // Update the source with filtered apps updatesBySource[source] = apps } func updateApp(_ app: UpdateableApp) async { switch app.source { case .homebrew: if let cask = app.appInfo.cask { GlobalConsoleManager.shared.appendOutput("Starting Homebrew update for \(app.appInfo.appName) (\(cask))...\n", source: CurrentPage.updater.title) // Update the app status if var apps = updatesBySource[.homebrew], let index = apps.firstIndex(where: { $0.id == app.id }) { apps[index].status = .downloading updatesBySource[.homebrew] = apps } // Perform upgrade do { try await HomebrewController.shared.upgradePackage(name: cask) GlobalConsoleManager.shared.appendOutput("✓ Successfully updated \(app.appInfo.appName) to version \(app.availableVersion ?? "unknown")\n", source: CurrentPage.updater.title) // Only remove from list if upgrade succeeded updatesBySource[.homebrew]?.removeAll { $0.id == app.id } // Refresh apps (only flush updated app's bundle for performance) await refreshApps(updatedApp: app.appInfo) } catch { GlobalConsoleManager.shared.appendOutput("✗ Failed to update \(app.appInfo.appName): \(error.localizedDescription)\n", source: CurrentPage.updater.title) // Update status to failed on error if var apps = updatesBySource[.homebrew], let index = apps.firstIndex(where: { $0.id == app.id }) { apps[index].status = .failed(error.localizedDescription) apps[index].progress = 0.0 // Reset progress indicator updatesBySource[.homebrew] = apps } printOS("Error updating Homebrew package \(cask): \(error)") } } case .appStore: if let adamID = app.adamID { GlobalConsoleManager.shared.appendOutput("Starting App Store update for \(app.appInfo.appName) (adamID: \(adamID))...\n", source: CurrentPage.updater.title) // Update the app status if var apps = updatesBySource[.appStore], let index = apps.firstIndex(where: { $0.id == app.id }) { apps[index].status = .downloading updatesBySource[.appStore] = apps } // Perform update (new API throws errors) do { try await AppStoreUpdater.shared.updateApp(adamID: adamID, appPath: app.appInfo.path, isIOSApp: app.isIOSApp) { [weak self] progress, status in Task { @MainActor in guard let self = self else { return } if var apps = self.updatesBySource[.appStore], let index = apps.firstIndex(where: { $0.id == app.id }) { apps[index].progress = progress // Update status based on App Store phase if status.contains("Downloading") || status.contains("Preparing") { // Phase 0 or 4: Downloading or preparing apps[index].status = .downloading self.updatesBySource[.appStore] = apps } else if status.contains("Installing") { // Phase 1: Installing apps[index].status = .installing self.updatesBySource[.appStore] = apps } else if status.contains("Completed") || status.contains("Already up to date") { // Phase 5 or no download needed: Complete - remove from list and refresh Task { await self.removeFromUpdatesList(appID: app.id, source: .appStore) await self.refreshApps(updatedApp: app.appInfo) } } else { // Other phases: Keep updating progress but maintain current status self.updatesBySource[.appStore] = apps } } } } // Update succeeded - refresh happens via completion callback above UpdaterDebugLogger.shared.log(.appStore, "✅ App Store update completed for adamID \(adamID)") GlobalConsoleManager.shared.appendOutput("✓ Successfully updated \(app.appInfo.appName) from App Store\n", source: CurrentPage.updater.title) } catch { // Handle errors from the new throwing API let message = error.localizedDescription printOS("❌ App Store update failed for adamID \(adamID): \(message)") GlobalConsoleManager.shared.appendOutput("✗ Failed to update \(app.appInfo.appName) from App Store: \(message)\n", source: CurrentPage.updater.title) // Update UI to show error (matching Sparkle's error display pattern) if var apps = updatesBySource[.appStore], let index = apps.firstIndex(where: { $0.id == app.id }) { apps[index].status = .failed(message) apps[index].progress = 0.0 updatesBySource[.appStore] = apps } } } case .sparkle: // Use Sparkle's updater via UpdateQueue to prevent concurrent update conflicts // SPUUpdater will automatically get feed URL from Info.plist via delegate // Check if update already queued/running for this app if UpdateQueue.shared.containsOperation(for: app.appInfo.bundleIdentifier) { UpdaterDebugLogger.shared.log(.sparkle, "⚠️ Update already queued for \(app.appInfo.appName)") printOS("Update already queued for \(app.appInfo.appName)") GlobalConsoleManager.shared.appendOutput("⚠ Update already queued for \(app.appInfo.appName)\n", source: CurrentPage.updater.title) return } GlobalConsoleManager.shared.appendOutput("Starting Sparkle update for \(app.appInfo.appName) (target version: \(app.availableVersion ?? "unknown"))...\n", source: CurrentPage.updater.title) UpdaterDebugLogger.shared.log(.sparkle, "═══ Initiating update for \(app.appInfo.appName)") UpdaterDebugLogger.shared.log(.sparkle, " Bundle ID: \(app.appInfo.bundleIdentifier)") UpdaterDebugLogger.shared.log(.sparkle, " Current version: \(app.appInfo.appVersion)") UpdaterDebugLogger.shared.log(.sparkle, " Target version: \(app.availableVersion ?? "unknown")") // Set initial downloading status updateStatus(for: app, status: .downloading, progress: 0.0) // Create Sparkle update operation (blocks until completion) let operation = SparkleUpdateOperation( app: app, includePreReleases: self.includeSparklePreReleases, progressCallback: { [weak self] progress, status in guard let self = self else { return } Task { @MainActor in self.updateStatus(for: app, status: status, progress: progress) } }, completionCallback: { [weak self] success, error in guard let self = self else { return } Task { @MainActor in if success { UpdaterDebugLogger.shared.log(.sparkle, "═══ Update completed successfully for \(app.appInfo.appName)") GlobalConsoleManager.shared.appendOutput("✓ Successfully updated \(app.appInfo.appName) via Sparkle\n", source: CurrentPage.updater.title) // Update completed - remove from list and refresh (only flush updated app's bundle) await self.removeFromUpdatesList(appID: app.id, source: .sparkle) await self.refreshApps(updatedApp: app.appInfo) } else { // Update failed - show error let message = error?.localizedDescription ?? "Unknown error" UpdaterDebugLogger.shared.log(.sparkle, "═══ Update failed for \(app.appInfo.appName): \(message)") GlobalConsoleManager.shared.appendOutput("✗ Failed to update \(app.appInfo.appName) via Sparkle: \(message)\n", source: CurrentPage.updater.title) self.updateStatus(for: app, status: .failed(message), progress: 0.0) } } } ) // Add to queue (limits concurrent operations to prevent Sparkle conflicts) UpdateQueue.shared.addOperation(operation) case .unsupported: // Unsupported apps cannot be updated - do nothing UpdaterDebugLogger.shared.log(.sparkle, "⚠️ Cannot update unsupported app: \(app.appInfo.appName)") break case .current: // Current apps are already up-to-date - do nothing UpdaterDebugLogger.shared.log(.sparkle, "ℹ️ App is already current: \(app.appInfo.appName)") break } } /// Update an iOS app from the App Store func updateIOSApp(_ app: UpdateableApp) async { guard app.isIOSApp, let adamID = app.adamID else { printOS("❌ Not an iOS app or missing adamID") GlobalConsoleManager.shared.appendOutput("✗ Not an iOS app or missing adamID for \(app.appInfo.appName)\n", source: CurrentPage.updater.title) return } GlobalConsoleManager.shared.appendOutput("Starting iOS app update for \(app.appInfo.appName) (adamID: \(adamID))...\n", source: CurrentPage.updater.title) // Update status if var apps = updatesBySource[.appStore], let index = apps.firstIndex(where: { $0.id == app.id }) { apps[index].status = .downloading apps[index].progress = 0.0 updatesBySource[.appStore] = apps } // Call AppStoreUpdater to download (which will trigger our observer) do { try await AppStoreUpdater.shared.updateApp( adamID: adamID, appPath: app.appInfo.path, isIOSApp: true, progress: { [weak self] progress, status in Task { @MainActor in guard let self = self else { return } if var apps = self.updatesBySource[.appStore], let index = apps.firstIndex(where: { $0.id == app.id }) { apps[index].progress = progress // Update status based on App Store phase (match real updateApp logic) if status.contains("Downloading") || status.contains("Preparing") { // Phase 0 or 4: Downloading or preparing apps[index].status = .downloading self.updatesBySource[.appStore] = apps } else if status.contains("Installing") { // Phase 1: Installing apps[index].status = .installing self.updatesBySource[.appStore] = apps } else if status.contains("Completed") || status.contains("Already up to date") { // Phase 5 or no download needed: Complete - remove from list and refresh Task { await self.removeFromUpdatesList(appID: app.id, source: .appStore) await self.refreshApps(updatedApp: app.appInfo) } } else { // Other phases: Keep updating progress but maintain current status self.updatesBySource[.appStore] = apps } } } } ) GlobalConsoleManager.shared.appendOutput("✓ Successfully updated iOS app \(app.appInfo.appName)\n", source: CurrentPage.updater.title) } catch { printOS("❌ iOS app update failed: \(error)") GlobalConsoleManager.shared.appendOutput("✗ Failed to update iOS app \(app.appInfo.appName): \(error.localizedDescription)\n", source: CurrentPage.updater.title) if var apps = updatesBySource[.appStore], let index = apps.firstIndex(where: { $0.id == app.id }) { apps[index].status = .failed("Open in App Store to update using the App Store button to the left.") updatesBySource[.appStore] = apps } } } func updateAll(source: UpdateSource) async { guard let apps = updatesBySource[source] else { return } // Only update apps that are selected for update let selectedApps = apps.filter { $0.isSelectedForUpdate } GlobalConsoleManager.shared.appendOutput("Starting batch update for \(selectedApps.count) app(s) from \(source.rawValue)...\n", source: CurrentPage.updater.title) for app in selectedApps { await updateApp(app) } GlobalConsoleManager.shared.appendOutput("Batch update completed for \(source.rawValue)\n", source: CurrentPage.updater.title) } /// Update all selected apps across all sources (concurrent per-source) func updateSelectedApps() async { // Count total selected apps across updateable sources only (exclude .current and .unsupported) let totalSelected = updatesBySource .filter { $0.key != .unsupported && $0.key != .current } .values .flatMap { $0 } .filter { $0.isSelectedForUpdate } .count totalAppsToUpdate = totalSelected isUpdatingAll = true defer { isUpdatingAll = false totalAppsToUpdate = 0 } GlobalConsoleManager.shared.appendOutput("Starting updates for \(totalSelected) selected app(s) across all sources...\n", source: CurrentPage.updater.title) await withTaskGroup(of: Void.self) { group in // Process each source's updates concurrently in separate Tasks for source in UpdateSource.allCases { if let apps = updatesBySource[source] { let selectedApps = apps.filter { $0.isSelectedForUpdate } if !selectedApps.isEmpty { group.addTask { // Within each source, process apps sequentially for app in selectedApps { await self.updateApp(app) } } } } } } GlobalConsoleManager.shared.appendOutput("All selected updates completed\n", source: CurrentPage.updater.title) } /// Update the status and progress of an app in the updates list private func updateStatus(for app: UpdateableApp, status: UpdateStatus, progress: Double) { if var apps = updatesBySource[app.source], let index = apps.firstIndex(where: { $0.id == app.id }) { apps[index].status = status apps[index].progress = progress updatesBySource[app.source] = apps } } // REMOVED: refreshSparkleAppWithURL - no longer needed with simplified Sparkle approach // Alternate feed URLs are not supported when using SPUUpdater directly /// Remove an app from the updates list private func removeFromUpdatesList(appID: UUID, source: UpdateSource) async { updatesBySource[source]?.removeAll { $0.id == appID } } /// Refresh all apps after an update /// - Parameter updatedApp: Optional specific app that was updated (only flushes that bundle for performance) private func refreshApps(updatedApp: AppInfo? = nil) async { let folderPaths = await MainActor.run { FolderSettingsManager.shared.folderPaths } // Only flush cache for the app that was just updated (or all if none specified) if let app = updatedApp { Pearcleaner.flushBundleCaches(for: [app]) } else { Pearcleaner.flushBundleCaches(for: AppState.shared.sortedApps) } await loadAppsAsync(folderPaths: folderPaths, useStreaming: false) } } ================================================ FILE: Pearcleaner/Logic/AppsUpdater/UpdateQueue.swift ================================================ // // UpdateQueue.swift // Pearcleaner // // Manages concurrent Sparkle update operations with proper queuing to prevent conflicts. // Limits concurrent operations to prevent Sparkle framework internal lock contention. // import Foundation class UpdateQueue { static let shared = UpdateQueue() private let queue: OperationQueue private init() { queue = OperationQueue() queue.maxConcurrentOperationCount = 3 // Limit concurrent Sparkle updates queue.qualityOfService = .userInitiated } /// Add a Sparkle update operation to the queue func addOperation(_ operation: Operation) { queue.addOperation(operation) } /// Cancel all pending operations func cancelAll() { queue.cancelAllOperations() } /// Check if an operation for a specific app is already queued or running func containsOperation(for bundleIdentifier: String) -> Bool { queue.operations.contains { operation in guard let sparkleOp = operation as? SparkleUpdateOperation else { return false } return sparkleOp.bundleIdentifier == bundleIdentifier } } } ================================================ FILE: Pearcleaner/Logic/AppsUpdater/UpdaterDebugLogger.swift ================================================ // // UpdaterDebugLogger.swift // Pearcleaner // // Created by Alin Lupascu on 10/25/25. // import Foundation import SwiftUI /// In-memory debug logger for updater sources /// Stores logs in memory when enabled, can be exported to file class UpdaterDebugLogger: ObservableObject { static let shared = UpdaterDebugLogger() // Separate log storage for each source @Published private(set) var appStoreLogs: [String] = [] @Published private(set) var sparkleLogs: [String] = [] @Published private(set) var homebrewLogs: [String] = [] private init() {} /// Check if debug logging is currently enabled (reads directly from UserDefaults) private var isDebugEnabled: Bool { UserDefaults.standard.object(forKey: "settings.updater.debugLogging") as? Bool ?? true } /// Log a message for a specific update source func log(_ source: UpdateSource, _ message: String) { guard isDebugEnabled else { return } let timestamp = DateFormatter.localizedString(from: Date(), dateStyle: .none, timeStyle: .medium) let logLine = "[\(timestamp)] \(message)" DispatchQueue.main.async { [weak self] in switch source { case .appStore: self?.appStoreLogs.append(logLine) case .sparkle: self?.sparkleLogs.append(logLine) case .homebrew: self?.homebrewLogs.append(logLine) case .unsupported: // No logs for unsupported apps (they don't have updates to debug) break case .current: // No logs for current apps (they're already up-to-date) break } } } /// Generate formatted debug output with all three sources func generateDebugReport() -> String { var report = "" report += "=" + String(repeating: "=", count: 78) + "\n" report += "UPDATER DEBUG LOG\n" report += "Generated: \(DateFormatter.localizedString(from: Date(), dateStyle: .medium, timeStyle: .medium))\n" report += "=" + String(repeating: "=", count: 78) + "\n\n" // App Store Section report += "━" + String(repeating: "━", count: 78) + "\n" report += "APP STORE UPDATE CHECKER\n" report += "━" + String(repeating: "━", count: 78) + "\n" if appStoreLogs.isEmpty { report += " (No logs recorded)\n" } else { for log in appStoreLogs { report += " \(log)\n" } } report += "\n" // Sparkle Section report += "━" + String(repeating: "━", count: 78) + "\n" report += "SPARKLE UPDATE DETECTOR\n" report += "━" + String(repeating: "━", count: 78) + "\n" if sparkleLogs.isEmpty { report += " (No logs recorded)\n" } else { for log in sparkleLogs { report += " \(log)\n" } } report += "\n" // Homebrew Section report += "━" + String(repeating: "━", count: 78) + "\n" report += "HOMEBREW UPDATE CHECKER\n" report += "━" + String(repeating: "━", count: 78) + "\n" if homebrewLogs.isEmpty { report += " (No logs recorded)\n" } else { for log in homebrewLogs { report += " \(log)\n" } } report += "\n" report += "=" + String(repeating: "=", count: 78) + "\n" report += "END OF DEBUG LOG\n" report += "=" + String(repeating: "=", count: 78) + "\n" return report } /// Clear all logs from memory func clearLogs() { DispatchQueue.main.async { [weak self] in self?.appStoreLogs.removeAll() self?.sparkleLogs.removeAll() self?.homebrewLogs.removeAll() } } /// Check if any logs exist var hasLogs: Bool { !appStoreLogs.isEmpty || !sparkleLogs.isEmpty || !homebrewLogs.isEmpty } } ================================================ FILE: Pearcleaner/Logic/AppsUpdater/UpdaterSettings.swift ================================================ // // UpdaterSettings.swift // Pearcleaner // // Created by Alin Lupascu on 11/21/25. // import Foundation // MARK: - AppStorage Codable Helper Protocol /// Protocol for types that can be stored in AppStorage as Data protocol AppStorageCodable: Codable { init() static func decode(from data: Data) -> Self func encode() -> Data static func defaultEncoded() -> Data } /// Default implementation for all AppStorageCodable types extension AppStorageCodable { /// Decode from Data with fallback to default static func decode(from data: Data) -> Self { (try? JSONDecoder().decode(Self.self, from: data)) ?? Self() } /// Encode to Data with fallback to empty func encode() -> Data { (try? JSONEncoder().encode(self)) ?? Data() } /// Default encoded value for AppStorage initialization static func defaultEncoded() -> Data { Self().encode() } } // MARK: - Sources Settings struct UpdaterSourcesSettings: AppStorageCodable { var homebrew: HomebrewSettings = HomebrewSettings() var sparkle: SparkleSettings = SparkleSettings() var appStore: AppStoreSettings = AppStoreSettings() struct HomebrewSettings: Codable { var enabled: Bool = true var showAutoUpdates: Bool = true } struct SparkleSettings: Codable { var enabled: Bool = true var includePreReleases: Bool = true } struct AppStoreSettings: Codable { var enabled: Bool = true } } // MARK: - Display Settings struct UpdaterDisplaySettings: AppStorageCodable { var showUnsupported: Bool = true var showCurrent: Bool = true } ================================================ FILE: Pearcleaner/Logic/AppsUpdater/VersionComparison.swift ================================================ // // VersionComparison.swift // Pearcleaner // // Created by Alin Lupascu on 10/22/25. // // Based on Sparkle Framework's version comparison algorithm // License: MIT // import Foundation /// A version struct that supports arbitrary-length version numbers (2, 3, 4+ components) /// Designed specifically for Sparkle update feeds which use wildly inconsistent formats struct Version: Hashable, Comparable { /// The user-facing version number (e.g., "1.2025.288.13", "1.0.0-beta") let versionNumber: String? /// The internal build number (e.g., "20251021184832000", "1234") let buildNumber: String? /// Flag indicating whether both version and build numbers are unavailable var isEmpty: Bool { let versionNumberComponents = versionNumber?.components().compactMap({ $0.plainComponent }).joined() let buildNumberComponents = buildNumber?.components().compactMap({ $0.plainComponent }).joined() return (versionNumberComponents?.isEmpty ?? true && buildNumberComponents?.isEmpty ?? true) } // MARK: - Comparisons static func ==(lhs: Version, rhs: Version) -> Bool { compare(lhs, rhs) == .equal } static func <(lhs: Version, rhs: Version) -> Bool { compare(lhs, rhs) == .older } static func >(lhs: Version, rhs: Version) -> Bool { compare(lhs, rhs) == .newer } // MARK: - Hashing func hash(into hasher: inout Hasher) { hasher.combine(versionNumber) hasher.combine(buildNumber) } // MARK: - Private /// An enum describing the result of a comparison private enum CheckingResult { case older, newer, equal, undefined } /// Performs the actual version comparison /// This algorithm is adopted from Sparkle Framework and slightly adapted private static func compare(_ lhs: Version, _ rhs: Version) -> CheckingResult { var v1: String? var v2: String? // Only allow build number checks if build and version number actually differ let allowBuildNumberCheck = lhs.buildNumber != lhs.versionNumber if allowBuildNumberCheck, let b1 = lhs.buildNumber, let b2 = rhs.buildNumber { v1 = b1 v2 = b2 } else { v1 = lhs.versionNumber v2 = rhs.versionNumber } guard let c1 = v1?.components(), let c2 = v2?.components() else { return .undefined } let count1 = c1.count let count2 = c2.count for i in 0.. value2 { return .newer // Think "1.3" vs "1.2" } else if value2 > value1 { return .older // Think "1.2" vs "1.3" } } // Compare letters else if case .string(let value1) = component1, case .string(let value2) = component2 { switch value1.compare(value2) { case .orderedAscending: return .older // Think "1.2A" vs "1.2B" case .orderedDescending: return .newer // Think "1.2B" vs "1.2A" default: () } } // Not the same type? Now we have to do some validity checking else if case .string(_) = component1 { return .older // Think "1.2A" vs "1.2.2" } else if case .string(_) = component2 { return .newer // Think "1.2.3" vs "1.2A" } // One is a number and the other is a period. The period is invalid else if case .number(_) = component1 { return .older // Think "1.2.." vs "1.2.0" } else if case .number(_) = component2 { return .newer // Think "1.2.3" vs "1.2.." } } } // The versions are equal up to the point where they both still have parts // Let's check to see if one is larger than the other if count1 != count2 { let l = count1 > count2 let longerComponents = (l ? c1 : c2)[(l ? count2 : count1)...] guard case .component(let atoms) = longerComponents.first(where: { if case .component(_) = $0 { true } else { false } }) else { return .equal // Think "1.2" vs "1.2." } if case .number(let number) = atoms.first { if number == 0 { return .equal // Think "1.2" vs "1.2.0" } return l ? .newer : .older // Think "1.2" vs "1.2.2" } return l ? .older : .newer // Think "1.2" vs "1.2A" } return .equal // Think "1.2" vs "1.2" } // MARK: - Version Sanitization /// Sanitizes version discrepancies between app version and remote version func sanitize(with appVersion: Version) -> Version { // Case 1: The last component of the version number is actually the build number // This can only be detected for equal build numbers to avoid false positives // Example: App has 1.2 (40), Remote has 1.2.40 // Action: Extract build number from version string → Version: 1.2, Build: 40 if buildNumber == nil, var components = versionNumber?.components(), let lastRemoteComponent = components.last?.plainComponent, lastRemoteComponent == appVersion.buildNumber { // Remove build number segment from version number and store it separately let buildNumber = components.removeLast() // Remove separator as well if !components.isEmpty { components.removeLast() } return Version(versionNumber: components.joined(), buildNumber: buildNumber.plainComponent) } // Case 2: The entire version number equals the app version's build number // We assume version number by default, but that may not be the case // Example: App has 1.2 (123), Remote has 123 // Action: Switch to build number comparison if let versionNumber, versionNumber == appVersion.buildNumber { return Version(versionNumber: nil, buildNumber: versionNumber) } // Case 3: Handle specific edge case with 7-component versions if appVersion.buildNumber == appVersion.versionNumber, var components = versionNumber?.components(), components.last?.plainComponent != nil, components.count == 7 { components.removeLast() components.removeLast() if components.joined() == appVersion.buildNumber { return Version(versionNumber: components.joined(), buildNumber: buildNumber) } } // Nothing changed - no sanitization needed return self } } // MARK: - Version Segment Parsing /// An extension helping with version parsing fileprivate extension String { /** Returns the components of a version number. Components are grouped by character type, so "12.3" returns [.component([.number(12)]), .separator("."), .component([.number(3)])] */ func components() -> [Version.Segment] { let scanner = Scanner(string: self) var components = [Version.Segment]() var currentAtoms = [Version.Segment.Atom]() while !scanner.isAtEnd { var number: Int = 0 // Try to scan number if scanner.scanInt(&number) { currentAtoms.append(.number(value: number)) } // Try to scan separator else if let string = scanner.scanCharacters(from: .separators) { components.append(.component(atoms: currentAtoms)) components.append(.separator(character: string as String)) currentAtoms.removeAll() } // Try to scan anything else (letters) else if let string = scanner.scanCharacters(from: .letters) { currentAtoms.append(.string(value: string as String)) } else { // Unable to parse - skip this character and continue scanner.currentIndex = scanner.string.index(after: scanner.currentIndex) } } if !currentAtoms.isEmpty { components.append(.component(atoms: currentAtoms)) } return components } } fileprivate extension CharacterSet { /// Contains all delimiters used by a version string static let separators = CharacterSet.whitespacesAndNewlines.union(.punctuationCharacters) /// Contains any characters but separators and digits static let letters = CharacterSet.separators.union(.decimalDigits).inverted } /// Defining the type of version segments fileprivate extension Version { enum Segment: Equatable { enum Atom: Equatable { case number(value: Int) // 0..9 case string(value: String) // Everything else (letters, text) func isSameType(_ other: Atom) -> Bool { switch (self, other) { case (.number(_), .number(_)), (.string(_), .string(_)): return true default: return false } } } case separator(character: String) // Newlines, punctuation (., -, etc.) case component(atoms: [Atom]) // [123, A] var plainComponent: String? { guard case .component(let atoms) = self else { return nil } return atoms.map { atom in switch atom { case .number(let value): return "\(value)" case .string(let value): return value } }.joined() } func isSameType(_ other: Segment) -> Bool { switch (self, other) { case (.separator, .separator), (.component(_), .component(_)): return true default: return false } } } } extension Array where Element == Version.Segment { func joined() -> String? { let string = self.map { segment in switch segment { case .separator(let character): character case .component(_): segment.plainComponent! } }.joined() return string.isEmpty ? nil : string } } // MARK: - Pre-Release Detection /// Detects if a version string contains pre-release indicators /// Checks for common patterns: beta, alpha, rc, pre, preview, dev, snapshot /// - Parameter versionString: The version string to check (e.g., "1.0.0-beta", "2.0rc1") /// - Returns: True if the version appears to be a pre-release func isPreReleaseVersion(_ versionString: String) -> Bool { let lowercased = versionString.lowercased() // Define pre-release keywords once (used for both patterns) let preReleaseKeywords = ["beta", "alpha", "rc", "pre", "preview", "dev", "snapshot"] // Pattern 1: Dash-separated (SemVer style) // Examples: "1.0.0-beta", "2.0-rc1", "3.0-alpha.2" for keyword in preReleaseKeywords { if lowercased.contains("-\(keyword)") { return true } } // Pattern 2: Text-based indicators without dash (less common but exists) // Examples: "1.2beta", "3.0alpha", "2.5rc1" for keyword in preReleaseKeywords { // Check if keyword appears after numbers (not at the start) // Use regex to ensure it's part of the version, not just in app name if lowercased.range(of: "\\d+.*\(keyword)", options: .regularExpression) != nil { // Found keyword after digits return true } } return false } ================================================ FILE: Pearcleaner/Logic/Brew/HomebrewAutoUpdateManager.swift ================================================ // // HomebrewAutoUpdateManager.swift // Pearcleaner // // Created by Alin Lupascu on 10/25/25. // import Foundation import AppKit import AlinFoundation import SwiftUI // MARK: - Plist State enum PlistState { case none // No plist file exists case active // Regular .plist exists } // MARK: - Schedule Model enum ScheduleFrequency: String, Codable, Hashable { case daily = "Daily" case weekly = "Weekly" case monthly = "Monthly" } struct ScheduleOccurrence: Identifiable, Codable, Hashable { let id: UUID var frequency: ScheduleFrequency var weekday: Int? // 0=Sunday, 1=Monday, ... 6=Saturday (used for weekly) var dayOfMonth: Int? // 1-28 (used for monthly) var hour: Int // 0-23 var minute: Int // 0-59 var isEnabled: Bool init(frequency: ScheduleFrequency = .weekly, weekday: Int? = nil, dayOfMonth: Int? = nil, hour: Int? = nil, minute: Int? = nil, isEnabled: Bool = true) { self.id = UUID() self.frequency = frequency // Use current date/time as defaults let now = Date() let calendar = Calendar.current let components = calendar.dateComponents([.weekday, .hour, .minute, .day], from: now) // Set defaults based on frequency switch frequency { case .daily: self.weekday = nil self.dayOfMonth = nil case .weekly: // Convert Calendar.weekday (1=Sunday...7=Saturday) to our format (0=Sunday...6=Saturday) let currentWeekday = (components.weekday ?? 1) - 1 self.weekday = weekday ?? currentWeekday self.dayOfMonth = nil case .monthly: self.weekday = nil self.dayOfMonth = dayOfMonth ?? min(components.day ?? 1, 28) // Cap at 28 for safety } self.hour = hour ?? (components.hour ?? 9) self.minute = minute ?? (components.minute ?? 0) self.isEnabled = isEnabled } } // MARK: - AutoUpdate Manager class HomebrewAutoUpdateManager: ObservableObject { static let shared = HomebrewAutoUpdateManager() @Published var schedules: [ScheduleOccurrence] = [] @Published var isAgentLoaded: Bool = false @Published var logFileExists: Bool = false // Master toggle stored in UserDefaults (independent of schedule existence) @AppStorage("settings.brew.autoUpdateEnabled") var isEnabled: Bool = false // Store schedules when disabled (for restoration when re-enabled) @AppStorage("settings.brew.autoUpdatePreservedSchedules") private var preservedSchedulesData: Data = Data() // Computed property to access preserved schedules private var preservedSchedules: [ScheduleOccurrence] { get { guard let decoded = try? JSONDecoder().decode([ScheduleOccurrence].self, from: preservedSchedulesData) else { return [] } return decoded } set { preservedSchedulesData = (try? JSONEncoder().encode(newValue)) ?? Data() } } // Global actions that apply to ALL schedules @Published var runUpdate: Bool = true @Published var runUpgrade: Bool = true @Published var runCleanup: Bool = false // Store original state for comparison (to detect edit mode) @Published var originalSchedules: [ScheduleOccurrence] = [] var originalRunUpdate: Bool = true var originalRunUpgrade: Bool = false var originalRunCleanup: Bool = false private let plistPath = "\(NSHomeDirectory())/Library/LaunchAgents/com.alienator88.Pearcleaner.homebrew-autoupdate.plist" private let label = "com.alienator88.Pearcleaner.homebrew-autoupdate" let logPath = "/tmp/homebrew-autoupdate.log" private init() { loadSchedule() checkAgentStatus() checkLogFiles() // Restore preserved schedules if disabled and no schedules loaded if !isEnabled && schedules.isEmpty && !preservedSchedules.isEmpty { schedules = preservedSchedules originalSchedules = preservedSchedules } // Auto-register agent if enabled with schedules but not running if isEnabled && !schedules.isEmpty && !isAgentLoaded { Task { do { try applySchedule() } catch { printOS("Failed to auto-register LaunchAgent on launch: \(error)") } } } } // MARK: - Public Methods /// Check which plist file exists (if any) var plistState: PlistState { let fileManager = FileManager.default if fileManager.fileExists(atPath: plistPath) { return .active } else { return .none } } /// Toggle the entire schedule on/off func toggleEnabled(_ enabled: Bool) throws { let fileManager = FileManager.default if enabled { // Enable: Restore preserved schedules if available isEnabled = true // Always restore from preserved schedules when re-enabling (not just when empty) // This ensures deleted schedules don't reappear after toggle cycle if !preservedSchedules.isEmpty { schedules = preservedSchedules originalSchedules = preservedSchedules } // Apply schedules immediately if we have at least one enabled schedule let enabledCount = schedules.filter { $0.isEnabled }.count if enabledCount > 0 { try applySchedule() } } else { // Disable: Preserve schedules but clean up plist isEnabled = false // Always save current schedules to AppStorage before cleanup (even if empty) // This ensures deleted schedules are cleared from AppStorage preservedSchedules = schedules // Unregister agent if running try? unregisterAgent() // Delete plist entirely (don't rename to .disabled) if fileManager.fileExists(atPath: plistPath) { try fileManager.removeItem(atPath: plistPath) } // Keep schedules in memory (they'll show dimmed in UI) } checkAgentStatus() } /// Check if a schedule is in "edit mode" (new or modified) /// Note: isEnabled is excluded - it auto-saves immediately and doesn't trigger edit mode func isInEditMode(_ schedule: ScheduleOccurrence) -> Bool { // New schedule not in originals = edit mode if !originalSchedules.contains(where: { $0.id == schedule.id }) { return true } // Check if any property changed from original (excluding isEnabled) guard let original = originalSchedules.first(where: { $0.id == schedule.id }) else { return true } return original.frequency != schedule.frequency || original.weekday != schedule.weekday || original.dayOfMonth != schedule.dayOfMonth || original.hour != schedule.hour || original.minute != schedule.minute } /// Save a single schedule to plist func saveSchedule(_ schedule: ScheduleOccurrence) throws { guard isEnabled else { return } // Update original state if let index = originalSchedules.firstIndex(where: { $0.id == schedule.id }) { originalSchedules[index] = schedule } else { originalSchedules.append(schedule) } // Also update action originals originalRunUpdate = runUpdate originalRunUpgrade = runUpgrade originalRunCleanup = runCleanup // Apply to plist and register LaunchAgent try applySchedule() } /// Revert schedule to original state (exit edit mode without saving) func revertSchedule(_ schedule: ScheduleOccurrence) { // Find original schedule guard let original = originalSchedules.first(where: { $0.id == schedule.id }) else { return } // Find index in current schedules guard let index = schedules.firstIndex(where: { $0.id == schedule.id }) else { return } // Revert to original values (excluding isEnabled which is instant-save) schedules[index].frequency = original.frequency schedules[index].weekday = original.weekday schedules[index].dayOfMonth = original.dayOfMonth schedules[index].hour = original.hour schedules[index].minute = original.minute } /// Delete schedule and save to plist func deleteSchedule(_ schedule: ScheduleOccurrence) throws { // Remove from current schedules schedules.removeAll { $0.id == schedule.id } // Remove from originals originalSchedules.removeAll { $0.id == schedule.id } // Always update preserved schedules in AppStorage (not just when disabled) // This ensures deletions persist across toggle cycles var preserved = preservedSchedules preserved.removeAll { $0.id == schedule.id } preservedSchedules = preserved // Apply to plist (saves deletion) try applySchedule() } /// Apply current schedules by generating plist and registering LaunchAgent func applySchedule() throws { // Don't apply if master toggle is disabled guard isEnabled else { return } // Check if we have any enabled schedules let enabledSchedules = schedules.filter { $0.isEnabled } guard !enabledSchedules.isEmpty else { // No enabled schedules - clean up completely try unregisterAgent() // Delete plist file (no schedules = no file needed) let fileManager = FileManager.default if fileManager.fileExists(atPath: plistPath) { try fileManager.removeItem(atPath: plistPath) } checkAgentStatus() return } // Generate plist content let plistContent = generatePlist() // Validate plist format guard let data = plistContent.data(using: .utf8) else { throw HomebrewAutoUpdateError.invalidEncoding } do { _ = try PropertyListSerialization.propertyList(from: data, options: [], format: nil) } catch { throw HomebrewAutoUpdateError.invalidFormat(error.localizedDescription) } // Ensure LaunchAgents directory exists let launchAgentsDir = (plistPath as NSString).deletingLastPathComponent let fileManager = FileManager.default if !fileManager.fileExists(atPath: launchAgentsDir) { try fileManager.createDirectory(atPath: launchAgentsDir, withIntermediateDirectories: true, attributes: nil) } // Write plist to LaunchAgents directory let plistURL = URL(fileURLWithPath: plistPath) try data.write(to: plistURL) // Verify file was written guard FileManager.default.fileExists(atPath: plistPath) else { throw HomebrewAutoUpdateError.fileWriteFailed } // Unregister old service (if exists) then register new one try? unregisterAgent(updateStatus: false) // Don't update UI during re-registration // Register with launchctl let uid = getuid() let bootstrapCommand = "launchctl bootstrap gui/\(uid) \(plistPath)" let result = shell(bootstrapCommand) if result.exitCode != 0 { throw HomebrewAutoUpdateError.registrationFailed(result.stderr) } // Update state checkAgentStatus() } /// Unregister LaunchAgent /// - Parameter updateStatus: Whether to update isAgentLoaded state (default: true) func unregisterAgent(updateStatus: Bool = true) throws { let uid = getuid() let bootoutCommand = "launchctl bootout gui/\(uid)/\(label)" let result = shell(bootoutCommand) // Note: bootout returns error if service not loaded, which is fine if result.exitCode != 0 && !result.stderr.contains("Could not find service") { throw HomebrewAutoUpdateError.registrationFailed(result.stderr) } // Only update status if requested (prevents UI flash during re-registration) if updateStatus { checkAgentStatus() } } /// Check if LaunchAgent is currently loaded func checkAgentStatus() { let uid = getuid() let printCommand = "launchctl print gui/\(uid)/\(label)" let result = shell(printCommand) // If launchctl print succeeds, the service is loaded isAgentLoaded = (result.exitCode == 0) } /// Check if log file exists func checkLogFiles() { let fileManager = FileManager.default logFileExists = fileManager.fileExists(atPath: logPath) } /// Refresh state by reloading schedule from plist and checking agent status func refreshState() { loadSchedule() checkAgentStatus() checkLogFiles() } /// Remove all new schedules that haven't been saved to plist func removeUnsavedSchedules() { schedules.removeAll { schedule in // Remove if this schedule is NOT in originalSchedules (meaning it's new and unsaved) !originalSchedules.contains(where: { $0.id == schedule.id }) } } /// Open log file in default text editor func openLogFile() { let url = URL(fileURLWithPath: logPath) NSWorkspace.shared.open(url) } /// Reload schedule from existing plist file (single source of truth) func loadSchedule() { let fileManager = FileManager.default // Only check regular plist path guard fileManager.fileExists(atPath: plistPath) else { schedules = [] return } // Read and parse plist guard let data = try? Data(contentsOf: URL(fileURLWithPath: plistPath)), let plist = try? PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any] else { printOS("Failed to read plist at \(plistPath)") schedules = [] return } // Extract StartCalendarInterval array guard let calendarIntervals = plist["StartCalendarInterval"] as? [[String: Int]] else { schedules = [] return } // Extract global actions from ProgramArguments (apply to all schedules) if let programArgs = plist["ProgramArguments"] as? [String], programArgs.count >= 3 { let command = programArgs[2] runUpdate = command.contains("brew update") runUpgrade = command.contains("brew upgrade") runCleanup = command.contains("brew autoremove") || command.contains("brew cleanup") } // Parse each interval into ScheduleOccurrence schedules = calendarIntervals.compactMap { interval in let hour = interval["Hour"] let minute = interval["Minute"] guard let hour = hour, let minute = minute else { return nil } // Determine frequency by which keys exist if let day = interval["Day"] { // Monthly (has Day key) return ScheduleOccurrence( frequency: .monthly, dayOfMonth: day, hour: hour, minute: minute, isEnabled: true ) } else if let weekday = interval["Weekday"] { // Weekly (has Weekday key) return ScheduleOccurrence( frequency: .weekly, weekday: weekday, hour: hour, minute: minute, isEnabled: true ) } else { // Daily (only Hour and Minute, no Weekday or Day) return ScheduleOccurrence( frequency: .daily, hour: hour, minute: minute, isEnabled: true ) } } // Store as original state after loading originalSchedules = schedules originalRunUpdate = runUpdate originalRunUpgrade = runUpgrade originalRunCleanup = runCleanup } // MARK: - Private Methods /// Execute shell command and return result private func shell(_ command: String) -> (stdout: String, stderr: String, exitCode: Int32) { let process = Process() process.executableURL = URL(fileURLWithPath: "/bin/sh") process.arguments = ["-c", command] let outputPipe = Pipe() let errorPipe = Pipe() process.standardOutput = outputPipe process.standardError = errorPipe do { try process.run() process.waitUntilExit() let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile() let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() let stdout = String(data: outputData, encoding: .utf8) ?? "" let stderr = String(data: errorData, encoding: .utf8) ?? "" return (stdout, stderr, process.terminationStatus) } catch { return ("", error.localizedDescription, -1) } } /// Generate plist XML from current schedules private func generatePlist() -> String { let enabledSchedules = schedules.filter { $0.isEnabled } // Build command as a shell script block for clean output let brewPath = HomebrewController.shared.getBrewPrefix() + "/bin/brew" var scriptLines: [String] = [] // Header scriptLines.append("echo \"\"") scriptLines.append("echo \"================================\"") scriptLines.append("echo \"Homebrew Auto-Update - $(date)\"") scriptLines.append("echo \"================================\"") scriptLines.append("echo \"\"") // Update section if runUpdate { scriptLines.append("echo \"[ Updating Homebrew ]\"") scriptLines.append("OUTPUT=$(\(brewPath) update 2>&1)") scriptLines.append("if [ -z \"$OUTPUT\" ]; then echo \"No action needed\"; else echo \"$OUTPUT\"; fi") scriptLines.append("echo \"\"") } // Upgrade section if runUpgrade { scriptLines.append("echo \"[ Upgrading Packages ]\"") scriptLines.append("OUTPUT=$(\(brewPath) upgrade --greedy 2>&1)") scriptLines.append("if [ -z \"$OUTPUT\" ]; then echo \"No action needed\"; else echo \"$OUTPUT\"; fi") scriptLines.append("echo \"\"") } // Cleanup section if runCleanup { scriptLines.append("echo \"[ Cleaning Up ]\"") scriptLines.append("OUTPUT=$(\(brewPath) autoremove 2>&1; \(brewPath) cleanup --scrub --prune=all 2>&1)") scriptLines.append("if [ -z \"$OUTPUT\" ]; then echo \"No action needed\"; else echo \"$OUTPUT\"; fi") scriptLines.append("echo \"\"") } // Footer scriptLines.append("echo \"================================\"") scriptLines.append("echo \"Completed at $(date)\"") scriptLines.append("echo \"================================\"") // Wrap in braces for single execution block and redirect to overwrite log file let scriptBlock = "{ " + scriptLines.joined(separator: "; ") + "; } > /tmp/homebrew-autoupdate.log 2>&1" // Escape XML special characters for plist let escapedCommand = scriptBlock .replacingOccurrences(of: "&", with: "&") .replacingOccurrences(of: "<", with: "<") .replacingOccurrences(of: ">", with: ">") .replacingOccurrences(of: "\"", with: """) // Generate StartCalendarInterval entries let calendarIntervals = enabledSchedules.map { schedule in var dict = """ """ // Add keys based on frequency switch schedule.frequency { case .daily: // Only Hour and Minute dict += """ Hour \(schedule.hour) Minute \(schedule.minute) """ case .weekly: // Weekday, Hour, Minute dict += """ Weekday \(schedule.weekday ?? 0) Hour \(schedule.hour) Minute \(schedule.minute) """ case .monthly: // Day, Hour, Minute dict += """ Day \(schedule.dayOfMonth ?? 1) Hour \(schedule.hour) Minute \(schedule.minute) """ } dict += """ """ return dict }.joined(separator: "\n") return """ Label \(label) ProgramArguments /bin/sh -c \(escapedCommand) EnvironmentVariables PATH \(HomebrewController.shared.getBrewPrefix())/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin SUDO_ASKPASS \(Bundle.main.bundlePath)/Contents/Resources/askpass.sh RunAtLoad StartCalendarInterval \(calendarIntervals) """ } } // MARK: - Error Types enum HomebrewAutoUpdateError: LocalizedError { case invalidEncoding case invalidFormat(String) case fileWriteFailed case registrationFailed(String) var errorDescription: String? { switch self { case .invalidEncoding: return "Failed to encode plist content" case .invalidFormat(let details): return "Invalid plist format: \(details)" case .fileWriteFailed: return "Failed to write plist file to LaunchAgents directory" case .registrationFailed(let details): return "Failed to register LaunchAgent: \(details)" } } } ================================================ FILE: Pearcleaner/Logic/Brew/HomebrewController.swift ================================================ // // HomebrewController.swift // Pearcleaner // // Created by Alin Lupascu on 10/01/25. // import Foundation import AlinFoundation enum HomebrewError: Error, LocalizedError { case brewNotFound case commandFailed(String) case jsonParseError case packageNotFound // User-actionable errors case dependencyConflict(package: String, dependents: [String]) case appAlreadyExists(package: String, path: String) case formulaConflict(package: String, conflicts: String) var errorDescription: String? { switch self { case .brewNotFound: return "Homebrew not found. Please install Homebrew first." case .commandFailed(let message): return message.trimmingCharacters(in: .whitespacesAndNewlines) case .jsonParseError: return "Failed to parse JSON response from Homebrew API" case .packageNotFound: return "Package not found in Homebrew" case .dependencyConflict(let package, let dependents): let depList = dependents.joined(separator: ", ") return "Cannot uninstall \(package) because it is required by: \(depList)" case .appAlreadyExists(let package, let path): return "Cannot install \(package) because an app already exists at: \(path)" case .formulaConflict(let package, let conflicts): return "Cannot install \(package) because of conflicts with: \(conflicts)" } } } // MARK: - OS Version Helpers private func getCurrentOSCodename() -> String { // TEMPORARY: Fake Sequoia for testing // return "sequoia" //MARK: THIS WILL NEED TO BE UDPATED WITH EACH NEW OS RELEASE let version = ProcessInfo.processInfo.operatingSystemVersion switch version.majorVersion { case 13: return "ventura" case 14: return "sonoma" case 15: return "sequoia" case 26...: return "tahoe" default: return "ventura" // fallback for unsupported versions } } private func extractVersionFromVariations( json: [String: Any], baseVersion: String?, osCodename: String, isArm: Bool ) -> String? { guard let variations = json["variations"] as? [String: Any] else { return baseVersion // No variations, use base } // Try arm64-specific first (e.g., "arm64_sequoia") if isArm, let armVariation = variations["arm64_\(osCodename)"] as? [String: Any], let version = armVariation["version"] as? String { return version } // Try OS-specific (e.g., "sequoia") if let osVariation = variations[osCodename] as? [String: Any], let version = osVariation["version"] as? String { return version } // Fallback to base version return baseVersion } // MARK: - Error Parsing Helpers func parseDependencyConflict(from error: String, package: String) -> HomebrewError? { // Pattern: "Refusing to uninstall ... because it is required by X, Y, which is currently installed" guard error.contains("Refusing to uninstall") && error.contains("because it is required by") else { return nil } // Extract dependents between "required by" and "which is currently installed" or end of line if let range = error.range(of: "required by ") { let afterBy = String(error[range.upperBound...]) // Find everything up to "which is currently installed" or newline let endRange = afterBy.range(of: ", which is") ?? afterBy.range(of: "\n") ?? afterBy.endIndex.. HomebrewError? { // Pattern: "It seems there is already an App at '/Applications/...'" guard error.contains("already an App at") || error.contains("already a") else { return nil } // Extract path between single quotes using simple string search if let startQuote = error.range(of: "'"), let endQuote = error.range(of: "'", range: startQuote.upperBound.. HomebrewError? { // Pattern: "Cannot install ... because conflicting formulae are installed" guard error.contains("Cannot install") && error.contains("conflicting formulae") else { return nil } // Extract conflicts - usually in the error message after "installed." // Simplified: just return the full conflict message return .formulaConflict(package: package, conflicts: "other installed formulae") } extension String { /// Strip all Homebrew revision suffixes and metadata from version string /// Used for both directory scan and API comparison to ensure consistent version matching /// Handles all common patterns: underscores, hyphens, commas, plus signs /// /// Keeps only alphanumeric characters and periods - strips from first suffix marker onward /// Then trims trailing non-alphanumeric characters (periods, etc.) /// /// Valid characters: digits (0-9), letters (a-z, A-Z), periods (.) /// Suffix markers: anything else (comma, plus, underscore, hyphen, etc.) /// /// Examples: /// - "0.14.1,fc796f5b" → "0.14.1" /// - "4.1.0+8404-main" → "4.1.0" /// - "2.14.1_1" → "2.14.1" /// - "8.27.2-4" → "8.27.2" /// - "141.0.7390.122-1.1" → "141.0.7390.122" /// - "1.0b5" → "1.0b5" (letters preserved) /// - "1.2.3a" → "1.2.3a" (pre-release preserved) /// - "v1.2.3" → "v1.2.3" (prefix preserved) /// - "1.2." → "1.2" (trailing period removed) func stripBrewRevisionSuffix() -> String { var result = self // Find first character that's not alphanumeric or period (suffix marker) if let firstSuffixIndex = result.firstIndex(where: { !$0.isLetter && !$0.isNumber && $0 != "." }) { result = String(result[.. other.version) /// return if version_comparison.nil? /// version_comparison.nonzero? || revision <=> other.revision static func < (lhs: PkgVersion, rhs: PkgVersion) -> Bool { // Use existing Version struct for semantic comparison let lhsVer = Version(versionNumber: lhs.version, buildNumber: nil) let rhsVer = Version(versionNumber: rhs.version, buildNumber: nil) // If versions differ, use version comparison if lhsVer != rhsVer { return lhsVer < rhsVer } // Versions equal, compare revisions return lhs.revision < rhs.revision } static func == (lhs: PkgVersion, rhs: PkgVersion) -> Bool { let lhsVer = Version(versionNumber: lhs.version, buildNumber: nil) let rhsVer = Version(versionNumber: rhs.version, buildNumber: nil) return lhsVer == rhsVer && lhs.revision == rhs.revision } } class HomebrewController: ObservableObject { static let shared = HomebrewController() private let brewPath: String let brewPrefix: String // Public for use in HomebrewUpdateChecker placeholder paths private let logger = UpdaterDebugLogger.shared // Track running operations for cancellation // Must be accessed/modified on main thread for SwiftUI observation @MainActor @Published var isOperationRunning: Bool = false @MainActor private var runningProcess: Process? // Console enabled flag (keep for backward compatibility) @MainActor @Published var consoleEnabled: Bool = false private init() { // Determine paths based on architecture if isOSArm() { self.brewPath = "/opt/homebrew/bin/brew" self.brewPrefix = "/opt/homebrew" } else { self.brewPath = "/usr/local/bin/brew" self.brewPrefix = "/usr/local" } } // MARK: - Installation Check var isInstalled: Bool { return FileManager.default.fileExists(atPath: brewPath) } // MARK: - Helper Methods func getBrewPrefix() -> String { return brewPrefix } // MARK: - Shell Command Execution /// Checks if command output indicates authentication failure private func isAuthenticationFailure(_ output: String) -> Bool { let indicators = [ "Sorry, try again", "incorrect password", "Authentication failure", "sudo: 3 incorrect password attempts", "sudo: no password was provided", "sudo: a password is required" ] return indicators.contains { output.lowercased().contains($0.lowercased()) } } /// Runs brew command with auto-retry on authentication failure func runBrewCommandWithRetry(_ arguments: [String], maxRetries: Int = 2) async throws -> (output: String, error: String) { var attemptCount = 0 while attemptCount < maxRetries { let (output, error) = try await runBrewCommand(arguments) // Check for authentication failure let combinedOutput = output + error if isAuthenticationFailure(combinedOutput) { printOS("🔐 Authentication failed, invalidating cache and retrying (attempt \(attemptCount + 1)/\(maxRetries))") KeychainPasswordManager.shared.invalidateCache() attemptCount += 1 if attemptCount < maxRetries { continue // Retry with fresh password } else { // Max retries reached, return the failed output printOS("❌ Authentication failed after \(maxRetries) attempts") return (output, error) } } // Success or non-auth error return (output, error) } // This shouldn't be reached, but return empty as fallback return ("", "Max retries reached") } func runBrewCommand(_ arguments: [String]) async throws -> (output: String, error: String) { // Mark operation as running - explicitly trigger SwiftUI update await MainActor.run { objectWillChange.send() isOperationRunning = true runningProcess = nil // Clear any stale process reference } let process = Process() process.executableURL = URL(fileURLWithPath: brewPath) process.arguments = arguments // Set up environment with SUDO_ASKPASS for password prompts during install/update var environment = ProcessInfo.processInfo.userEnvironment let askpassPath = "\(Bundle.main.bundlePath)/Contents/Resources/askpass.sh" environment["SUDO_ASKPASS"] = askpassPath process.environment = environment let outputPipe = Pipe() let errorPipe = Pipe() process.standardOutput = outputPipe process.standardError = errorPipe // Store process reference for cancellation await MainActor.run { runningProcess = process } try process.run() // Read pipes on background thread with console streaming let (outputData, errorData) = await withCheckedContinuation { continuation in Task.detached { var outputData = Data() var errorData = Data() let outputHandle = outputPipe.fileHandleForReading let errorHandle = errorPipe.fileHandleForReading // Read output with streaming to console while true { let chunk = outputHandle.availableData if chunk.isEmpty { break } outputData.append(chunk) // Stream to console if enabled - check dynamically to support mid-operation console opening if let text = String(data: chunk, encoding: .utf8) { await MainActor.run { GlobalConsoleManager.shared.appendOutput(text, source: CurrentPage.homebrew.title) } } } // Read error output errorData = errorHandle.readDataToEndOfFile() continuation.resume(returning: (outputData, errorData)) } } process.waitUntilExit() // Clear running state - explicitly trigger SwiftUI update await MainActor.run { objectWillChange.send() isOperationRunning = false runningProcess = nil } let output = String(data: outputData, encoding: .utf8) ?? "" let error = String(data: errorData, encoding: .utf8) ?? "" return (output, error) } /// Cancel the currently running Homebrew operation @MainActor func cancelOperation() { guard let process = runningProcess else { return } isOperationRunning = false process.terminate() runningProcess = nil } // MARK: - Package Loading /// Stream installed packages by scanning Cellar/Caskroom directories /// Returns minimal info: name + displayName + description + version + isPinned + tap + tapRbPath func streamInstalledPackages( cask: Bool, onPackageFound: @escaping (String, String?, String, String, Bool, String?, String?, Bool) -> Void // (name, displayName, description, version, isPinned, tap, tapRbPath, installedOnRequest) ) async throws { let baseDir = cask ? "\(brewPrefix)/Caskroom" : "\(brewPrefix)/Cellar" logger.log(.homebrew, "🔍 Scanning for installed \(cask ? "casks" : "formulae") in \(baseDir)") await MainActor.run { GlobalConsoleManager.shared.appendOutput("Loading installed \(cask ? "casks" : "formulae")...\n", source: CurrentPage.homebrew.title) } guard let packageDirs = try? FileManager.default.contentsOfDirectory(atPath: baseDir) else { logger.log(.homebrew, "⚠️ Could not read directory: \(baseDir)") return } let packageCount = packageDirs.filter { !$0.hasPrefix(".") }.count logger.log(.homebrew, "Found \(packageCount) \(cask ? "casks" : "formulae") to process") // Process concurrently, stream results as they complete var loadedCount = 0 await withTaskGroup(of: (String, String?, String, String, Bool, String?, String?, Bool)?.self) { group in // Add all tasks for packageName in packageDirs where !packageName.hasPrefix(".") { group.addTask { if cask { // Casks are always considered installed on request if let result = await self.getCaskNameDescVersionPin(name: packageName) { return (result.0, result.1, result.2, result.3, result.4, result.5, result.6, true) } return nil } else { return await self.getFormulaNameDescVersionPin(name: packageName) } } } // Collect results as they complete for await result in group { if let (name, displayName, desc, version, isPinned, tap, tapRbPath, installedOnRequest) = result { onPackageFound(name, displayName, desc, version, isPinned, tap, tapRbPath, installedOnRequest) loadedCount += 1 } } } let finalLoadedCount = loadedCount await MainActor.run { GlobalConsoleManager.shared.appendOutput("Loaded \(finalLoadedCount) \(cask ? "casks" : "formulae")\n", source: CurrentPage.homebrew.title) } } /// Load minimal package metadata (name, displayName, description, version) from local JWS files /// Much faster than API calls and works offline /// JWS files are already cached by Homebrew after `brew update` func loadMinimalPackageMetadata(cask: Bool) async throws -> [(name: String, displayName: String?, description: String?, version: String?, bundleVersion: String?)] { await MainActor.run { GlobalConsoleManager.shared.appendOutput("Loading available \(cask ? "casks" : "formulae") metadata...\n", source: CurrentPage.homebrew.title) } let fileName = cask ? "cask.jws.json" : "formula.jws.json" let apiCachePath = FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent("Library/Caches/Homebrew/api") let jwsFilePath = apiCachePath.appendingPathComponent(fileName).path guard FileManager.default.fileExists(atPath: jwsFilePath) else { throw HomebrewError.commandFailed("JWS file not found: \(fileName). Run 'brew update' first.") } // Read JWS file let jwsContent = try String(contentsOfFile: jwsFilePath, encoding: .utf8) // Parse JWS structure: {"payload": "json-string-array", "signatures": [...]} // Note: payload is NOT base64-encoded, it's a plain JSON string guard let jwsData = jwsContent.data(using: .utf8), let jwsJson = try JSONSerialization.jsonObject(with: jwsData) as? [String: Any], let payloadString = jwsJson["payload"] as? String, let payloadData = payloadString.data(using: .utf8), let payloadArray = try JSONSerialization.jsonObject(with: payloadData) as? [[String: Any]] else { throw HomebrewError.jsonParseError } var results: [(name: String, displayName: String?, description: String?, version: String?, bundleVersion: String?)] = [] // Extract package metadata from array for packageDict in payloadArray { let name: String let displayName: String? let description = packageDict["desc"] as? String let version: String? let bundleVersion: String? if cask { // Casks: token is brew ID, name is array with display name guard let token = packageDict["token"] as? String else { continue } name = token let nameArray = packageDict["name"] as? [String] displayName = nameArray?.first // Extract version with OS-specific variation support let rawVersion = packageDict["version"] as? String bundleVersion = packageDict["bundle_version"] as? String // Check for OS-specific version in variations let osCodename = getCurrentOSCodename() #if arch(arm64) let isArm = true #else let isArm = false #endif version = extractVersionFromVariations( json: packageDict, baseVersion: rawVersion, osCodename: osCodename, isArm: isArm ) } else { // Formulae: name is brew ID (no separate display name) guard let formulaName = packageDict["name"] as? String else { continue } name = formulaName displayName = nil // Formulae don't have separate display names version = (packageDict["versions"] as? [String: Any])?["stable"] as? String bundleVersion = nil // Formulae don't have bundle versions } results.append((name: name, displayName: displayName, description: description, version: version, bundleVersion: bundleVersion)) } let resultsCount = results.count await MainActor.run { GlobalConsoleManager.shared.appendOutput("Loaded \(resultsCount) available \(cask ? "casks" : "formulae")\n", source: CurrentPage.homebrew.title) } return results } /// Load package names from text files (formula_names.txt or cask_names.txt) /// Returns array of package names only - no descriptions or other metadata /// Falls back to .before.txt files if current files don't exist (e.g., after brew update) func loadPackageNames(cask: Bool) async throws -> [String] { let fileName = cask ? "cask_names.txt" : "formula_names.txt" let beforeFileName = cask ? "cask_names.before.txt" : "formula_names.before.txt" let apiCachePath = FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent("Library/Caches/Homebrew/api") // Try current file first let currentFilePath = apiCachePath.appendingPathComponent(fileName).path let beforeFilePath = apiCachePath.appendingPathComponent(beforeFileName).path // Determine which file to use let filePathToUse: String if FileManager.default.fileExists(atPath: currentFilePath) { filePathToUse = currentFilePath } else if FileManager.default.fileExists(atPath: beforeFilePath) { filePathToUse = beforeFilePath } else { throw HomebrewError.commandFailed("Neither \(fileName) nor \(beforeFileName) found") } // Read file content let content = try String(contentsOfFile: filePathToUse, encoding: .utf8) // Split by newlines and filter empty lines let names = content.components(separatedBy: .newlines) .map { $0.trimmingCharacters(in: .whitespaces) } .filter { !$0.isEmpty } guard !names.isEmpty else { throw HomebrewError.commandFailed("Package names file is empty: \(filePathToUse)") } return names } /// Extract name, displayName, description, version, and pin status from formula func getFormulaNameDescVersionPin(name: String) async -> (String, String?, String, String, Bool, String?, String?, Bool)? { let cellarPath = "\(brewPrefix)/Cellar/\(name)" // Find latest version directory guard let versions = try? FileManager.default.contentsOfDirectory(atPath: cellarPath) .filter({ !$0.hasPrefix(".") }), let latestVersion = versions.sorted().last else { return nil } // Check if pinned (pin file exists) let pinPath = "\(brewPrefix)/var/homebrew/pinned/\(name)" let isPinned = FileManager.default.fileExists(atPath: pinPath) // Read INSTALL_RECEIPT.json for installed_on_request field only // For version: ALWAYS use directory name (includes revision suffix like "2.14.1_1") // INSTALL_RECEIPT stores base version without revision, so it's unreliable for revision tracking let receiptPath = "\(cellarPath)/\(latestVersion)/INSTALL_RECEIPT.json" var installedOnRequest = false // Default to false if field missing if let receiptData = try? Data(contentsOf: URL(fileURLWithPath: receiptPath)), let receipt = try? JSONSerialization.jsonObject(with: receiptData) as? [String: Any] { installedOnRequest = receipt["installed_on_request"] as? Bool ?? false } // Use directory name as version (already includes revision if present) let cleanedVersion = latestVersion // Read .rb file for description let rbPath = "\(cellarPath)/\(latestVersion)/.brew/\(name).rb" var desc = "No description available" if let rbContent = try? String(contentsOfFile: rbPath) { // Parse desc with regex: desc "..." let descRegex = /desc "([^"]+)"/ if let match = rbContent.firstMatch(of: descRegex) { desc = String(match.1) } } // Don't load tap info during scan - will be lazy loaded during outdated check if needed let tap: String? = nil let tapRbPath: String? = nil // Formulae don't have separate display names let displayName: String? = nil return (name, displayName, desc, cleanedVersion, isPinned, tap, tapRbPath, installedOnRequest) } /// Get runtime dependencies for a formula from INSTALL_RECEIPT.json func getRuntimeDependencies(formulaName: String) -> [String] { let cellarPath = "\(brewPrefix)/Cellar/\(formulaName)" guard let versions = try? FileManager.default.contentsOfDirectory(atPath: cellarPath) .filter({ !$0.hasPrefix(".") }), let latestVersion = versions.sorted().last else { return [] } let receiptPath = "\(cellarPath)/\(latestVersion)/INSTALL_RECEIPT.json" guard let receiptData = try? Data(contentsOf: URL(fileURLWithPath: receiptPath)), let receipt = try? JSONSerialization.jsonObject(with: receiptData) as? [String: Any], let runtimeDeps = receipt["runtime_dependencies"] as? [[String: Any]] else { return [] } var deps: [String] = [] for dep in runtimeDeps { if let fullName = dep["full_name"] as? String { deps.append(fullName) } } return deps } /// Extract name, displayName, description, version, and pin status from cask private func getCaskNameDescVersionPin(name: String) async -> (String, String?, String, String, Bool, String?, String?)? { let caskroomPath = "\(brewPrefix)/Caskroom/\(name)" // Skip symlinks (like xcodes -> xcodes-app) if let attrs = try? FileManager.default.attributesOfItem(atPath: caskroomPath), let fileType = attrs[.type] as? FileAttributeType, fileType == .typeSymbolicLink { return nil } // Use glob pattern to find the cask file: .metadata/*/*/Casks/.* let metadataPath = "\(caskroomPath)/.metadata" let globPattern = "\(metadataPath)/*/*/Casks/\(name).*" var globResult = glob_t() defer { globfree(&globResult) } guard glob(globPattern, 0, nil, &globResult) == 0, globResult.gl_pathc > 0, let firstPath = globResult.gl_pathv[0], let caskFilePath = String(validatingUTF8: firstPath) else { return nil } // Extract version from path: .metadata///Casks/... let pathComponents = caskFilePath.components(separatedBy: "/") var version: String? = nil if let metadataIndex = pathComponents.lastIndex(of: ".metadata"), metadataIndex + 1 < pathComponents.count { version = pathComponents[metadataIndex + 1] } guard let finalVersion = version else { return nil } // Strip revision suffix from version (e.g., "3.3.14_1" -> "3.3.14") let cleanedVersion = finalVersion.stripBrewRevisionSuffix() // Casks don't support pinning let isPinned = false // Read displayName and description from the cask file (.rb or .json) var displayName: String? = nil var desc = "No description available" if let fileContent = try? String(contentsOfFile: caskFilePath) { // Extract name (display name) - casks can have multiple names, take first let nameRegex = /name "([^"]+)"/ if let match = fileContent.firstMatch(of: nameRegex) { displayName = String(match.1) } // Extract description let descRegex = /desc "([^"]+)"/ if let match = fileContent.firstMatch(of: descRegex) { desc = String(match.1) } } // Read tap info from INSTALL_RECEIPT.json let receiptPath = "\(caskroomPath)/.metadata/INSTALL_RECEIPT.json" var tap: String? = nil var tapRbPath: String? = nil if let receiptData = try? Data(contentsOf: URL(fileURLWithPath: receiptPath)), let receipt = try? JSONSerialization.jsonObject(with: receiptData) as? [String: Any] { // Try new format first (source.tap and source.path) if let source = receipt["source"] as? [String: Any] { tap = source["tap"] as? String tapRbPath = source["path"] as? String } // Fall back to old format (top-level tap field) else if let topLevelTap = receipt["tap"] as? String { tap = topLevelTap } } return (name, displayName, desc, cleanedVersion, isPinned, tap, tapRbPath) } // MARK: - Search /// Fetch type-safe package details from Homebrew API /// Returns either FormulaDetails or CaskDetails wrapped in PackageDetailsType func getPackageDetailsTyped(name: String, cask: Bool) async throws -> PackageDetailsType { // First check Homebrew's local cache let cacheDir = FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent("Library/Caches/Homebrew/api") let cacheFile = cask ? cacheDir.appendingPathComponent("cask/\(name).json") : cacheDir.appendingPathComponent("formula/\(name).json") let data: Data // Check local cache first (faster) if FileManager.default.fileExists(atPath: cacheFile.path) { data = try Data(contentsOf: cacheFile) } else { // Fetch from API let url = cask ? URL(string: "https://formulae.brew.sh/api/cask/\(name).json")! : URL(string: "https://formulae.brew.sh/api/formula/\(name).json")! (data, _) = try await URLSession.shared.data(from: url) } guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { throw HomebrewError.jsonParseError } if cask { return .cask(try parseCaskDetails(json: json, name: name)) } else { return .formula(try parseFormulaDetails(json: json, name: name)) } } private func parseFormulaDetails(json: [String: Any], name: String) throws -> FormulaDetails { // Common fields let description = json["desc"] as? String let homepage = json["homepage"] as? String let license = json["license"] as? String let version = (json["versions"] as? [String: Any])?["stable"] as? String let caveats = json["caveats"] as? String let dependencies = (json["dependencies"] as? [String]) ?? [] let conflicts = (json["conflicts_with"] as? [String]) ?? [] let conflictsReasons = (json["conflicts_with_reasons"] as? [String]) ?? [] let tap = json["tap"] as? String let fullName = json["full_name"] as? String let deprecated = (json["deprecated"] as? Bool) ?? false let deprecationDate = json["deprecation_date"] as? String let deprecationReason = json["deprecation_reason"] as? String let disabled = (json["disabled"] as? Bool) ?? false let disableDate = json["disable_date"] as? String let disableReason = json["disable_reason"] as? String // Formula-specific fields let kegOnly = json["keg_only"] as? Bool let kegOnlyReason: String? if let kegOnlyReasonDict = json["keg_only_reason"] as? [String: Any], let explanation = kegOnlyReasonDict["explanation"] as? String, !explanation.isEmpty { kegOnlyReason = explanation } else if let kegOnlyReasonDict = json["keg_only_reason"] as? [String: Any], let reason = kegOnlyReasonDict["reason"] as? String { switch reason { case ":provided_by_macos": kegOnlyReason = "macOS already provides this software" case ":versioned_formula": kegOnlyReason = "This is a versioned formula" case ":shadowed_by_macos": kegOnlyReason = "Shadowed by macOS" default: kegOnlyReason = "Not symlinked to Homebrew prefix" } } else { kegOnlyReason = nil } let requirements = (json["requirements"] as? [String]) ?? [] let buildDependencies = (json["build_dependencies"] as? [String]) ?? [] let optionalDependencies = (json["optional_dependencies"] as? [String]) ?? [] let recommendedDependencies = (json["recommended_dependencies"] as? [String]) ?? [] let usesFromMacos = (json["uses_from_macos"] as? [Any])?.compactMap { item -> String? in if let str = item as? String { return str } else if let dict = item as? [String: Any], let key = dict.keys.first { // Handle {"bison": "build"} format - just show the name return key } return nil } ?? [] let versionedFormulae = (json["versioned_formulae"] as? [String]) ?? [] let aliases = (json["aliases"] as? [String]) ?? [] // Service info (only if actually defined, not just null) let service: ServiceInfo? if let serviceDict = json["service"] as? [String: Any], !serviceDict.isEmpty { let run = (serviceDict["run"] as? [String]) ?? [] let runType = serviceDict["run_type"] as? String let workingDir = serviceDict["working_dir"] as? String let keepAlive = (serviceDict["keep_alive"] as? [String: Any])?["always"] as? Bool // Only create ServiceInfo if there's actual data if !run.isEmpty || runType != nil || workingDir != nil || keepAlive != nil { service = ServiceInfo(run: run.isEmpty ? nil : run, runType: runType, workingDir: workingDir, keepAlive: keepAlive) } else { service = nil } } else { service = nil } // Replacement suggestions let deprecationReplacementFormula = json["deprecation_replacement_formula"] as? String let deprecationReplacementCask = json["deprecation_replacement_cask"] as? String let disableReplacementFormula = json["disable_replacement_formula"] as? String let disableReplacementCask = json["disable_replacement_cask"] as? String return FormulaDetails( name: name, description: description, homepage: homepage, license: license, version: version, dependencies: dependencies.isEmpty ? nil : dependencies, caveats: caveats, tap: tap, fullName: fullName, isDeprecated: deprecated, deprecationReason: deprecationReason, deprecationDate: deprecationDate, isDisabled: disabled, disableDate: disableDate, disableReason: disableReason, conflictsWith: conflicts.isEmpty ? nil : conflicts, conflictsWithReasons: conflictsReasons.isEmpty ? nil : conflictsReasons, isBottled: version != nil, isKegOnly: kegOnly, kegOnlyReason: kegOnlyReason, buildDependencies: buildDependencies.isEmpty ? nil : buildDependencies, optionalDependencies: optionalDependencies.isEmpty ? nil : optionalDependencies, recommendedDependencies: recommendedDependencies.isEmpty ? nil : recommendedDependencies, usesFromMacos: usesFromMacos.isEmpty ? nil : usesFromMacos, aliases: aliases.isEmpty ? nil : aliases, versionedFormulae: versionedFormulae.isEmpty ? nil : versionedFormulae, requirements: requirements.isEmpty ? nil : requirements.joined(separator: ", "), service: service, deprecationReplacementFormula: deprecationReplacementFormula, deprecationReplacementCask: deprecationReplacementCask, disableReplacementFormula: disableReplacementFormula, disableReplacementCask: disableReplacementCask ) } private func parseCaskDetails(json: [String: Any], name: String) throws -> CaskDetails { // Common fields let description = json["desc"] as? String let homepage = json["homepage"] as? String let license = json["license"] as? String let version = json["version"] as? String let caveats = json["caveats"] as? String let dependencies = ((json["depends_on"] as? [String: Any])?["formula"] as? [String]) ?? [] let conflicts = (json["conflicts_with"] as? [String]) ?? [] let conflictsReasons = (json["conflicts_with_reasons"] as? [String]) ?? [] let tap = json["tap"] as? String let fullName = (json["full_token"] as? String) ?? (json["token"] as? String) let deprecated = (json["deprecated"] as? Bool) ?? false let deprecationDate = json["deprecation_date"] as? String let deprecationReason = json["deprecation_reason"] as? String let disabled = (json["disabled"] as? Bool) ?? false let disableDate = json["disable_date"] as? String let disableReason = json["disable_reason"] as? String // Cask-specific fields let caskName = (json["name"] as? [String]) ?? [] let autoUpdates = json["auto_updates"] as? Bool let artifacts = (json["artifacts"] as? [[String: Any]])?.compactMap { $0.keys.first } let url = json["url"] as? String let appcast = json["appcast"] as? String let bundleVersion = json["bundle_version"] as? String let bundleShortVersion = json["bundle_short_version"] as? String // System requirements let minimumMacOSVersion: String? if let dependsOn = json["depends_on"] as? [String: Any], let macosDict = dependsOn["macos"] as? [String: Any], let firstKey = macosDict.keys.first { let versionArray = macosDict[firstKey] as? [String] ?? [] minimumMacOSVersion = "\(firstKey) \(versionArray.first ?? "")" } else { minimumMacOSVersion = nil } let architectureRequirement: ArchRequirement? if let dependsOn = json["depends_on"] as? [String: Any], let archArray = dependsOn["arch"] as? [String] { if archArray.contains("x86_64") && archArray.contains("arm64") { architectureRequirement = .universal } else if archArray.contains("x86_64") { architectureRequirement = .intel } else if archArray.contains("arm64") { architectureRequirement = .arm } else { architectureRequirement = nil } } else { architectureRequirement = nil } // Replacement suggestions let deprecationReplacementFormula = json["deprecation_replacement_formula"] as? String let deprecationReplacementCask = json["deprecation_replacement_cask"] as? String let disableReplacementFormula = json["disable_replacement_formula"] as? String let disableReplacementCask = json["disable_replacement_cask"] as? String return CaskDetails( name: name, description: description, homepage: homepage, license: license, version: version, dependencies: dependencies.isEmpty ? nil : dependencies, caveats: caveats, tap: tap, fullName: fullName, isDeprecated: deprecated, deprecationReason: deprecationReason, deprecationDate: deprecationDate, isDisabled: disabled, disableDate: disableDate, disableReason: disableReason, conflictsWith: conflicts.isEmpty ? nil : conflicts, conflictsWithReasons: conflictsReasons.isEmpty ? nil : conflictsReasons, caskName: caskName.isEmpty ? nil : caskName, autoUpdates: autoUpdates, artifacts: artifacts?.isEmpty == false ? artifacts : nil, url: url, appcast: appcast, minimumMacOSVersion: minimumMacOSVersion, architectureRequirement: architectureRequirement, bundleVersion: bundleVersion, bundleShortVersion: bundleShortVersion, deprecationReplacementFormula: deprecationReplacementFormula, deprecationReplacementCask: deprecationReplacementCask, disableReplacementFormula: disableReplacementFormula, disableReplacementCask: disableReplacementCask ) } func getAnalytics(name: String, cask: Bool) async throws -> HomebrewAnalytics { let cacheDir = FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent("Library/Caches/Homebrew/api") let cacheFile = cask ? cacheDir.appendingPathComponent("cask/\(name).json") : cacheDir.appendingPathComponent("formula/\(name).json") let data: Data // Check local cache first (Homebrew's cache) if FileManager.default.fileExists(atPath: cacheFile.path) { data = try Data(contentsOf: cacheFile) } else { // Fetch from API (Homebrew will cache it automatically) let url = cask ? URL(string: "https://formulae.brew.sh/api/cask/\(name).json")! : URL(string: "https://formulae.brew.sh/api/formula/\(name).json")! (data, _) = try await URLSession.shared.data(from: url) } guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], let analytics = json["analytics"] as? [String: Any] else { throw HomebrewError.jsonParseError } if cask { // Cask: simpler structure {"install": {"30d": {"name": 123}}} let install = analytics["install"] as? [String: Any] let install30d = (install?["30d"] as? [String: Int])?.values.first let install90d = (install?["90d"] as? [String: Int])?.values.first let install365d = (install?["365d"] as? [String: Int])?.values.first return HomebrewAnalytics( install30d: install30d, install90d: install90d, install365d: install365d ) } else { // Formula: only fetch install counts (not install_on_request or build_error) let install = analytics["install"] as? [String: Any] let install30d = (install?["30d"] as? [String: Int])?.values.reduce(0, +) let install90d = (install?["90d"] as? [String: Int])?.values.reduce(0, +) let install365d = (install?["365d"] as? [String: Int])?.values.reduce(0, +) return HomebrewAnalytics( install30d: install30d, install90d: install90d, install365d: install365d ) } } // MARK: - Package Management func installPackage(name: String, cask: Bool, force: Bool = false) async throws { logger.log(.homebrew, "📦 Installing package: \(name) (type: \(cask ? "cask" : "formula"))") var arguments = ["install"] if cask { arguments.append("--cask") arguments.append("--no-quarantine") if force { arguments.append("--force") } } else { arguments.append("--formula") if force { arguments.append("--force") } } arguments.append(name) do { let result = try await runBrewCommand(arguments) // Check for actual errors (not warnings) let combinedOutput = result.output + result.error if result.error.contains("Error:") && !combinedOutput.contains("was successfully installed") { logger.log(.homebrew, "❌ Install failed for \(name): \(result.error)") // Parse specific errors if let appExistsError = parseAppAlreadyExists(from: result.error, package: name) { throw appExistsError } if let conflictError = parseFormulaConflict(from: result.error, package: name) { throw conflictError } // Fallback to generic error throw HomebrewError.commandFailed(result.error) } logger.log(.homebrew, "✓ Installed \(name) successfully") } catch { logger.log(.homebrew, "❌ Install failed for \(name): \(error.localizedDescription)") throw error } } func adoptCask(token: String) async throws { logger.log(.homebrew, "🔄 Adopting cask: \(token)") let arguments = ["install", "--cask", "--adopt", "--no-quarantine", token] do { let result = try await runBrewCommand(arguments) // Check for actual errors (not warnings) let combinedOutput = result.output + result.error if result.error.contains("Error:") && !combinedOutput.contains("was successfully installed") { logger.log(.homebrew, "❌ Adoption failed for \(token): \(result.error)") // Parse specific errors (reuse existing error parsers) if let conflictError = parseFormulaConflict(from: result.error, package: token) { throw conflictError } // Fallback to generic error throw HomebrewError.commandFailed(result.error) } logger.log(.homebrew, "✓ Adopted \(token) successfully") } catch { logger.log(.homebrew, "❌ Adoption failed for \(token): \(error.localizedDescription)") throw error } } func uninstallPackage(name: String, ignoreDependencies: Bool = false) async throws { logger.log(.homebrew, "🗑️ Uninstalling package: \(name)") // Check if pinned and unpin automatically (user is choosing to uninstall, pin doesn't matter) let pinPath = "\(brewPrefix)/var/homebrew/pinned/\(name)" if FileManager.default.fileExists(atPath: pinPath) { logger.log(.homebrew, "📌 Package is pinned, unpinning before uninstall...") try await unpinPackage(name: name) } var arguments = ["uninstall", name] if ignoreDependencies { arguments.append("--ignore-dependencies") } do { let result = try await runBrewCommand(arguments) if result.error.contains("Error") || result.error.contains("because it is required by") { logger.log(.homebrew, "❌ Uninstall failed for \(name): \(result.error)") // Parse specific errors if let depError = parseDependencyConflict(from: result.error, package: name) { throw depError } // Fallback to generic error throw HomebrewError.commandFailed(result.error) } logger.log(.homebrew, "✓ Uninstalled \(name) successfully") } catch { logger.log(.homebrew, "❌ Uninstall failed for \(name): \(error.localizedDescription)") throw error } } func pinPackage(name: String) async throws { let arguments = ["pin", name] let result = try await runBrewCommand(arguments) if !result.error.isEmpty && result.error.contains("Error") { throw HomebrewError.commandFailed(result.error) } } func unpinPackage(name: String) async throws { let arguments = ["unpin", name] let result = try await runBrewCommand(arguments) if !result.error.isEmpty && result.error.contains("Error") { throw HomebrewError.commandFailed(result.error) } } func upgradePackage(name: String) async throws { logger.log(.homebrew, "⬆️ Upgrading package: \(name)") let arguments = ["upgrade", name] do { let result = try await runBrewCommand(arguments) if result.error.contains("Error") { logger.log(.homebrew, "❌ Upgrade failed for \(name): \(result.error)") throw HomebrewError.commandFailed(result.error) } logger.log(.homebrew, "✓ Upgraded \(name) successfully") } catch { logger.log(.homebrew, "❌ Upgrade failed for \(name): \(error.localizedDescription)") throw error } } func upgradeAllPackages() async throws { let arguments = ["upgrade"] let result = try await runBrewCommand(arguments) if result.error.contains("Error") { throw HomebrewError.commandFailed(result.error) } } /// Outdated package information with versions struct HomebrewOutdatedPackage { let name: String let installedVersion: String let availableVersion: String let isPinned: Bool let isCask: Bool } /// Get outdated packages using hybrid approach: API for core packages, .rb file reading for tap packages /// Much faster than `brew outdated` (~3.5x speedup) for core packages, accurate for tap packages /// Returns only packages that have updates available func getOutdatedPackagesHybrid(formulae: [InstalledPackage], casks: [InstalledPackage]) async -> [HomebrewOutdatedPackage] { let allPackages = formulae + casks logger.log(.homebrew, "Starting Homebrew update check for \(allPackages.count) packages (\(formulae.count) formulae, \(casks.count) casks)") await MainActor.run { GlobalConsoleManager.shared.appendOutput("Checking for outdated packages (\(allPackages.count) total)...\n", source: CurrentPage.homebrew.title) } // Step 1: Try to check ALL packages via API first (fast path) // Assume packages with tap == nil are core packages (most common case) logger.log(.homebrew, "Step 1: Checking packages via public API (fast path)") await MainActor.run { GlobalConsoleManager.shared.appendOutput("Checking packages via API...\n", source: CurrentPage.homebrew.title) } let (coreOutdated, apiFailedPackages) = await checkCorePackagesViaAPI(allPackages) logger.log(.homebrew, " API check complete: \(coreOutdated.count) outdated, \(apiFailedPackages.count) API failures (likely tap packages)") // Step 2: For packages where API failed, lazy-load tap info and check manually // This handles tap packages that don't exist in public API (typically 0-3 packages) if !apiFailedPackages.isEmpty { logger.log(.homebrew, "Step 2: Checking \(apiFailedPackages.count) tap packages manually") await MainActor.run { GlobalConsoleManager.shared.appendOutput("Checking \(apiFailedPackages.count) tap packages...\n", source: CurrentPage.homebrew.title) } let tapOutdated = await checkTapPackagesManually(apiFailedPackages) logger.log(.homebrew, " Manual tap check complete: \(tapOutdated.count) outdated") let totalOutdated = coreOutdated.count + tapOutdated.count logger.log(.homebrew, "Found \(totalOutdated) Homebrew updates available") await MainActor.run { GlobalConsoleManager.shared.appendOutput("Found \(totalOutdated) outdated packages\n", source: CurrentPage.homebrew.title) } // Filter out Pearcleaner (has dedicated UI banner in Updater view) let allOutdated = coreOutdated + tapOutdated return allOutdated.filter { $0.name != "pearcleaner" } } logger.log(.homebrew, "Found \(coreOutdated.count) Homebrew updates available") await MainActor.run { GlobalConsoleManager.shared.appendOutput("Found \(coreOutdated.count) outdated packages\n", source: CurrentPage.homebrew.title) } // Filter out Pearcleaner (has dedicated UI banner in Updater view) return coreOutdated.filter { $0.name != "pearcleaner" } } /// Check core Homebrew packages using public API (fast) /// Returns tuple: (outdatedPackages, apiFailedPackages) private func checkCorePackagesViaAPI(_ packages: [InstalledPackage]) async -> (outdated: [HomebrewOutdatedPackage], apiFailed: [InstalledPackage]) { // Fetch latest versions from API using parallel requests let latestVersions = await withTaskGroup(of: (String, String?, String?, Int?, Bool).self, returning: [String: (String, String?, Int?, Bool)].self) { group in for package in packages { group.addTask { // Construct API URL based on package type let urlString = package.isCask ? "https://formulae.brew.sh/api/cask/\(package.name).json" : "https://formulae.brew.sh/api/formula/\(package.name).json" guard let url = URL(string: urlString) else { return (package.name, nil, nil, nil, package.isCask) } // Use cache policy to bypass HTTP cache (prevents stale API data after upgrades) // Homebrew API returns Cache-Control: max-age=600 (10 minutes) let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalAndRemoteCacheData) guard let (data, _) = try? await URLSession.shared.data(for: request), let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return (package.name, nil, nil, nil, package.isCask) } if package.isCask { // Casks: Extract version and bundle version let rawVersion = json["version"] as? String let bundleVersion = json["bundle_version"] as? String // Check for OS-specific version in variations let osCodename = getCurrentOSCodename() #if arch(arm64) let isArm = true #else let isArm = false #endif let versionToUse = extractVersionFromVariations( json: json, baseVersion: rawVersion, osCodename: osCodename, isArm: isArm ) // Strip revision suffix for casks (handles Sparkle updates) let version = versionToUse?.stripBrewRevisionSuffix() return (package.name, version, bundleVersion, nil, true) } else { // Formulae: Extract version and revision separately let rawVersion = (json["versions"] as? [String: Any])?["stable"] as? String let revision = json["revision"] as? Int ?? 0 // Build full version with revision for formulae (don't strip) let version: String? if let rawVersion = rawVersion { version = revision > 0 ? "\(rawVersion)_\(revision)" : rawVersion } else { version = nil } return (package.name, version, nil, revision, false) } } } // Collect results into dictionary (version, bundleVersion, revision, isCask) var results: [String: (String, String?, Int?, Bool)] = [:] for await (name, version, bundleVersion, revision, isCask) in group { if let version = version { results[name] = (version, bundleVersion, revision, isCask) } } return results } // Track packages that failed API lookup and build outdated list var apiFailedPackages: [InstalledPackage] = [] var outdatedPackages: [HomebrewOutdatedPackage] = [] // Compare installed vs latest and build outdated package list for package in packages { guard let installedVersion = package.version else { continue // No installed version } // For casks, use hybrid version detection to handle both Sparkle updates and incomplete app versions // Compare app bundle version vs Homebrew metadata version and use the HIGHER one let actualVersion: String let installedBundleVersion: String? if package.isCask { // Find matching app in sortedApps by cask name (must access on MainActor) let appInfo = await MainActor.run { AppState.shared.sortedApps.first(where: { $0.cask == package.name }) } if let appInfo = appInfo { let appBundleVersion = appInfo.appVersion let homebrewMetadataVersion = installedVersion // Already cleaned via stripBrewRevisionSuffix() // Compare using Version struct and use HIGHER version // This handles: // - Apps with incomplete versions (Google Drive: app=116.0, brew=116.0.6 → use 116.0.6) // - Apps with Sparkle updates (app=116.0.7, brew=116.0.6 → use 116.0.7) let appVer = Version(versionNumber: appBundleVersion, buildNumber: nil) let brewVer = Version(versionNumber: homebrewMetadataVersion, buildNumber: nil) actualVersion = (appVer > brewVer) ? appBundleVersion : homebrewMetadataVersion installedBundleVersion = appInfo.appBuildNumber // CFBundleVersion for tiebreaker logger.log(.homebrew, " 🔍 Cask \(package.name): app=\(appBundleVersion), brew=\(homebrewMetadataVersion), using=\(actualVersion) (build: \(installedBundleVersion ?? "nil"))") } else { actualVersion = installedVersion // Fallback to Homebrew metadata if app not found installedBundleVersion = nil logger.log(.homebrew, " ⚠️ App not found in sortedApps for cask \(package.name), using Homebrew metadata: \(installedVersion)") } } else { actualVersion = installedVersion // For formulae, use Homebrew metadata (no Info.plist) installedBundleVersion = nil } if let (latestVersion, apiBundleVersion, _, _) = latestVersions[package.name] { // API call succeeded - package exists in public API // Determine if update is available var isOutdated = false if package.isCask { // Casks: Strip revision suffix and use Version struct (handles Sparkle updates) let installedClean = actualVersion.stripBrewRevisionSuffix() let availableClean = latestVersion.stripBrewRevisionSuffix() let installed = Version(versionNumber: installedClean, buildNumber: nil) let available = Version(versionNumber: availableClean, buildNumber: nil) if !installed.isEmpty && !available.isEmpty { if available > installed { // Clear case: API version is newer isOutdated = true } else if available == installed { // Versions are equal - use bundle version as tiebreaker if let installedBundle = installedBundleVersion, let apiBundle = apiBundleVersion { let installedBundleVer = Version(versionNumber: installedBundle, buildNumber: nil) let apiBundleVer = Version(versionNumber: apiBundle, buildNumber: nil) if apiBundleVer > installedBundleVer { isOutdated = true logger.log(.homebrew, " 🔍 Version equal, using bundle version tiebreaker: \(installedBundle) → \(apiBundle)") } } } } } else { // Formulae: Use PkgVersion for revision-aware comparison (no stripping) let installed = PkgVersion(actualVersion) let available = PkgVersion(latestVersion) // PkgVersion handles empty/invalid versions internally via Version struct if available > installed { isOutdated = true } } // Only mark outdated if update is available // This prevents false positives where app is actually newer than API (Sparkle updated ahead) if isOutdated { logger.log(.homebrew, " 📦 UPDATE AVAILABLE: \(package.name) - \(actualVersion) → \(latestVersion) (\(package.isCask ? "cask" : "formula"))") outdatedPackages.append(HomebrewOutdatedPackage( name: package.name, installedVersion: actualVersion, // Use actual version for display availableVersion: latestVersion, isPinned: package.isPinned, isCask: package.isCask )) } else { logger.log(.homebrew, " ✓ Up to date: \(package.name) (actual: \(actualVersion), available: \(latestVersion))") } } else { // API call failed - likely a tap package logger.log(.homebrew, " ⚠️ API lookup failed for \(package.name) - will check manually") apiFailedPackages.append(package) } } return (outdatedPackages, apiFailedPackages) } /// Check tap packages by reading their .rb files directly (accurate, like Homebrew does) /// Lazy-loads tap info from INSTALL_RECEIPT.json on-demand private func checkTapPackagesManually(_ packages: [InstalledPackage]) async -> [HomebrewOutdatedPackage] { var outdatedPackages: [HomebrewOutdatedPackage] = [] for package in packages { logger.log(.homebrew, " Checking tap package: \(package.name)") guard let installedVersion = package.version else { logger.log(.homebrew, " ⚠️ Skipped - no installed version found") continue // Can't check without installed version } // Lazy-load tap info from INSTALL_RECEIPT if not already cached var rbPath = package.tapRbPath if rbPath == nil { // Read INSTALL_RECEIPT.json to get tap .rb file path let receiptPath: String if package.isCask { receiptPath = "\(brewPrefix)/Caskroom/\(package.name)/.metadata/INSTALL_RECEIPT.json" } else { // For formulae, need to find the version directory let cellarPath = "\(brewPrefix)/Cellar/\(package.name)" guard let versions = try? FileManager.default.contentsOfDirectory(atPath: cellarPath) .filter({ !$0.hasPrefix(".") }), let latestVersion = versions.sorted().last else { continue } receiptPath = "\(cellarPath)/\(latestVersion)/INSTALL_RECEIPT.json" } // Try to read tap rb path from INSTALL_RECEIPT if let receiptData = try? Data(contentsOf: URL(fileURLWithPath: receiptPath)), let receipt = try? JSONSerialization.jsonObject(with: receiptData) as? [String: Any], let source = receipt["source"] as? [String: Any], let path = source["path"] as? String { rbPath = path } } // If we still don't have an rb path, skip this package guard let finalRbPath = rbPath else { logger.log(.homebrew, " ⚠️ Skipped - no .rb file path found") continue } logger.log(.homebrew, " Reading .rb file: \(finalRbPath)") // Read the tap's .rb file guard let rbContent = try? String(contentsOfFile: finalRbPath) else { logger.log(.homebrew, " ❌ Failed to read .rb file") continue // Rb file not readable } // Parse version and revision from .rb file using line-by-line search // Look for lines that ONLY contain: version "X.Y.Z" or revision N // This avoids matching comments or other occurrences var tapVersion: String? var tapRevision: Int = 0 for line in rbContent.split(separator: "\n") { let trimmed = line.trimmingCharacters(in: .whitespaces) // Match standalone version declarations: version "X.Y.Z" // Pattern ensures it's on its own line (Ruby requirement) let versionRegex = /^version\s+"([^"]+)"$/ if let match = trimmed.firstMatch(of: versionRegex) { // Strip revision suffix for casks to match installed version format tapVersion = package.isCask ? String(match.1).stripBrewRevisionSuffix() : String(match.1) continue // Keep searching for revision } // Match standalone revision declarations: revision N let revisionRegex = /^revision\s+(\d+)$/ if let match = trimmed.firstMatch(of: revisionRegex) { tapRevision = Int(match.1) ?? 0 } } // If version not found, skip this package (don't show as outdated) guard let baseVersion = tapVersion else { logger.log(.homebrew, " ⚠️ No standalone version line found - skipping") continue } // Build full version with revision (same as API and directory naming) let availableVersion = tapRevision > 0 ? "\(baseVersion)_\(tapRevision)" : baseVersion logger.log(.homebrew, " Tap version from .rb: \(availableVersion) (base: \(baseVersion), revision: \(tapRevision))") // Compare using PkgVersion for revision-aware comparison (formulae and casks in taps) let installed = PkgVersion(installedVersion) let available = PkgVersion(availableVersion) logger.log(.homebrew, " Comparing: \(installedVersion) vs \(availableVersion)") // Only add if truly outdated guard available > installed else { logger.log(.homebrew, " ✓ Up to date") continue } logger.log(.homebrew, " 📦 UPDATE AVAILABLE: \(installedVersion) → \(availableVersion)") outdatedPackages.append(HomebrewOutdatedPackage( name: package.name, installedVersion: installedVersion, availableVersion: availableVersion, isPinned: package.isPinned, isCask: package.isCask )) } return outdatedPackages } // MARK: - Tap Management func loadTaps() async throws -> [HomebrewTapInfo] { // Read taps directly from filesystem instead of calling `brew tap` // This avoids unwanted console output during background operations // Mimics Homebrew's Tap.installed logic: /opt/homebrew/Library/Taps/ let tapsDirectory = "\(brewPrefix)/Library/Taps" let fileManager = FileManager.default guard fileManager.fileExists(atPath: tapsDirectory) else { return [] } var tapNames: [String] = [] // Get all user/org directories let userDirs = try fileManager.contentsOfDirectory(atPath: tapsDirectory) .filter { !$0.hasPrefix(".") } for userDir in userDirs { let userPath = "\(tapsDirectory)/\(userDir)" // Get all repo directories for this user let repoDirs = try fileManager.contentsOfDirectory(atPath: userPath) .filter { !$0.hasPrefix(".") && fileManager.fileExists(atPath: "\(userPath)/\($0)/.git") } for repoDir in repoDirs { // Strip "homebrew-" prefix from repo name let repoName = repoDir.hasPrefix("homebrew-") ? String(repoDir.dropFirst("homebrew-".count)) : repoDir // Combine as "user/repo" tapNames.append("\(userDir)/\(repoName)") } } return tapNames.map { name in let isOfficial = name.starts(with: "homebrew/") return HomebrewTapInfo(name: name, isOfficial: isOfficial) } } func addTap(name: String) async throws { let arguments = ["tap", name] let result = try await runBrewCommand(arguments) if result.error.contains("Error") { throw HomebrewError.commandFailed(result.error) } } func removeTap(name: String, force: Bool = false) async throws { var arguments = ["untap"] if force { arguments.append("--force") } arguments.append(name) let result = try await runBrewCommand(arguments) if !result.error.contains("Untapped") && result.error.contains("Error") { throw HomebrewError.commandFailed(result.error) } } // MARK: - Maintenance func getBrewVersion() async throws -> String { await MainActor.run { GlobalConsoleManager.shared.appendOutput("Getting Homebrew version...\n", source: CurrentPage.homebrew.title) } // Use git directly for faster version check (avoids spawning brew process) // --abbrev=0 returns clean semantic version (e.g., "4.6.19") for consistent display // Works with both full clones and shallow clones let gitCommand = "git -C \(brewPrefix) describe --tags --abbrev=0 2>/dev/null" let process = Process() process.executableURL = URL(fileURLWithPath: "/bin/sh") process.arguments = ["-c", gitCommand] let pipe = Pipe() process.standardOutput = pipe process.standardError = Pipe() try process.run() process.waitUntilExit() let data = pipe.fileHandleForReading.readDataToEndOfFile() let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" if process.terminationStatus == 0 && !output.isEmpty { await MainActor.run { GlobalConsoleManager.shared.appendOutput("Homebrew version: \(output)\n", source: CurrentPage.homebrew.title) } return output // Returns "4.6.19" } // Fallback to brew command if git fails let arguments = ["-v"] let result = try await runBrewCommand(arguments) let components = result.output.components(separatedBy: " ") if components.count >= 2 { let version = components[1].trimmingCharacters(in: .whitespacesAndNewlines) // Extract semantic version from potential full string (e.g., "4.6.19-22-ga6c4bc4" -> "4.6.19") if let match = version.range(of: #"^\d+\.\d+\.\d+"#, options: .regularExpression) { return String(version[match]) } return version } return "Unknown" } func getLatestBrewVersionFromGitHub() async throws -> String { let url = URL(string: "https://api.github.com/repos/Homebrew/brew/releases/latest")! let (data, _) = try await URLSession.shared.data(from: url) guard let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], let tagName = json["tag_name"] as? String else { throw HomebrewError.jsonParseError } return tagName } func checkForBrewUpdate() async throws -> (current: String, latest: String, updateAvailable: Bool) { await MainActor.run { GlobalConsoleManager.shared.appendOutput("Checking for Homebrew updates...\n", source: CurrentPage.homebrew.title) } // Get current semantic version (e.g., "4.6.19") let currentVersion = try await getBrewVersion() // Get latest version from GitHub releases let latestVersion = try await getLatestBrewVersionFromGitHub() // Compare semantic versions let updateAvailable = compareSemanticVersions(current: currentVersion, latest: latestVersion) await MainActor.run { if updateAvailable { GlobalConsoleManager.shared.appendOutput("Update available: \(currentVersion) → \(latestVersion)\n", source: CurrentPage.homebrew.title) } else { GlobalConsoleManager.shared.appendOutput("Homebrew is up to date (\(currentVersion))\n", source: CurrentPage.homebrew.title) } } return (current: currentVersion, latest: latestVersion, updateAvailable: updateAvailable) } private func compareSemanticVersions(current: String, latest: String) -> Bool { let currentComponents = current.split(separator: ".").compactMap { Int($0) } let latestComponents = latest.split(separator: ".").compactMap { Int($0) } guard currentComponents.count >= 3, latestComponents.count >= 3 else { printOS("Brew version check - Semantic comparison failed: invalid version format. Current components: \(currentComponents.count), Latest components: \(latestComponents.count)") return false } for i in 0.. currentComponents[i] { return true } else if latestComponents[i] < currentComponents[i] { return false } } return false } func updateBrew() async throws { let arguments = ["update", "-v"] let result = try await runBrewCommand(arguments) if result.error.contains("Error") { throw HomebrewError.commandFailed(result.error) } } func runDoctor() async throws -> String { let arguments = ["doctor"] let result = try await runBrewCommand(arguments) return result.output + result.error } func runCleanup(dryRun: Bool = false) async throws -> (bytes: Int64, formatted: String)? { // Collect all cleanable cache and log files (or calculate their size if dry-run) let homeDir = FileManager.default.homeDirectoryForCurrentUser let cacheDir = homeDir.appendingPathComponent("Library/Caches/Homebrew") let cacheSubdirs = ["Cask", "api-source", "gh-actions-artifact", "cargo_cache", "go_cache", "go_mod_cache", "glide_home", "java_cache", "npm_cache", "pip_cache", "gclient_cache"] let logsDir = homeDir.appendingPathComponent("Library/Logs/Homebrew") let fileManager = FileManager.default var filesToDelete: [URL] = [] var totalBytes: Int64 = 0 // 1. Everything in downloads/ folder let downloadsDir = cacheDir.appendingPathComponent("downloads") if fileManager.fileExists(atPath: downloadsDir.path) { do { let downloadFiles = try fileManager.contentsOfDirectory(at: downloadsDir, includingPropertiesForKeys: nil, options: []) if dryRun { for file in downloadFiles { totalBytes += totalSizeOnDisk(for: file) } } else { filesToDelete.append(contentsOf: downloadFiles) } } catch { // Continue if we can't read downloads directory } } // 2. Additional cache subdirectories (emulate brew cleanup --prune=all) // brew's nested_cache? removes entire directories with FileUtils.rm_rf for subdirName in cacheSubdirs { let subdirURL = cacheDir.appendingPathComponent(subdirName) if fileManager.fileExists(atPath: subdirURL.path) { if dryRun { totalBytes += totalSizeOnDisk(for: subdirURL) } else { // Delete entire subdirectory (brew uses FileUtils.rm_rf on nested_cache directories) await MainActor.run { GlobalConsoleManager.shared.appendOutput("Removing \(subdirName)/\n", source: CurrentPage.homebrew.title) } filesToDelete.append(subdirURL) } } } // 3. Non-directory files and versioned directories in root Homebrew cache folder if fileManager.fileExists(atPath: cacheDir.path) { do { let contents = try fileManager.contentsOfDirectory(at: cacheDir, includingPropertiesForKeys: [.isDirectoryKey], options: []) for itemURL in contents { let resourceValues = try itemURL.resourceValues(forKeys: [.isDirectoryKey]) if resourceValues.isDirectory == false { // Skip .cleaned file (Homebrew's periodic cleanup tracker) if itemURL.lastPathComponent != ".cleaned" { if dryRun { totalBytes += totalSizeOnDisk(for: itemURL) } else { filesToDelete.append(itemURL) } } } else if itemURL.lastPathComponent.contains("--") { // Also remove directories with "--" (old formula/cask version caches, HEAD installs) if dryRun { totalBytes += totalSizeOnDisk(for: itemURL) } else { filesToDelete.append(itemURL) } } } } catch { // Continue if we can't read cache directory } } // 4. Everything in logs directory if fileManager.fileExists(atPath: logsDir.path) { if dryRun { totalBytes += totalSizeOnDisk(for: logsDir) } else { do { let logFiles = try fileManager.contentsOfDirectory(at: logsDir, includingPropertiesForKeys: nil, options: []) filesToDelete.append(contentsOf: logFiles) } catch { // Continue if we can't read logs directory } } } // Return results based on mode if dryRun { // Format as human-readable (must run on main thread - ByteCountFormatter is not thread-safe) let bytesToFormat = totalBytes let formatted = await MainActor.run { ByteCountFormatter.string(fromByteCount: bytesToFormat, countStyle: .file) } return (bytes: totalBytes, formatted: formatted) } else { // Move all files to Trash in a bundle if !filesToDelete.isEmpty { let itemCount = filesToDelete.count await MainActor.run { GlobalConsoleManager.shared.appendOutput("Cleaning \(itemCount) items...\n", source: CurrentPage.homebrew.title) } let _ = FileManagerUndo.shared.deleteFiles(at: filesToDelete, bundleName: "BrewCleanup") await MainActor.run { GlobalConsoleManager.shared.appendOutput("Cleanup complete\n", source: CurrentPage.homebrew.title) } } else { await MainActor.run { GlobalConsoleManager.shared.appendOutput("No files to clean\n", source: CurrentPage.homebrew.title) } } return nil } } func performFullCleanup() async throws { // Fast operation: delete cache and logs to Trash (blocks UI briefly ~50ms) _ = try await runCleanup() // Slow operation: run brew autoremove in background without blocking UI Task.detached(priority: .background) { let autoremoveArgs = ["autoremove"] _ = try? await HomebrewController.shared.runBrewCommand(autoremoveArgs) } } func getAnalyticsStatus() async throws -> Bool { await MainActor.run { GlobalConsoleManager.shared.appendOutput("Checking analytics status...\n", source: CurrentPage.homebrew.title) } // Use git config directly for faster check (avoids spawning brew process) let gitCommand = "git -C \(brewPrefix) config --get homebrew.analyticsdisabled 2>/dev/null" let process = Process() process.executableURL = URL(fileURLWithPath: "/bin/sh") process.arguments = ["-c", gitCommand] let pipe = Pipe() process.standardOutput = pipe process.standardError = Pipe() try process.run() process.waitUntilExit() let data = pipe.fileHandleForReading.readDataToEndOfFile() let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" // If config key doesn't exist or is empty, analytics are enabled by default // If set to "true", analytics are disabled // If set to "false", analytics are enabled let analyticsEnabled: Bool if output.isEmpty { analyticsEnabled = true // Analytics enabled by default } else { analyticsEnabled = output.lowercased() != "true" // Return true if NOT disabled } await MainActor.run { GlobalConsoleManager.shared.appendOutput("Analytics are \(analyticsEnabled ? "enabled" : "disabled")\n", source: CurrentPage.homebrew.title) } return analyticsEnabled } func setAnalyticsStatus(enabled: Bool) async throws { await MainActor.run { GlobalConsoleManager.shared.appendOutput("Setting analytics to \(enabled ? "enabled" : "disabled")...\n", source: CurrentPage.homebrew.title) } // Use git config directly for faster toggle (avoids spawning brew process) let value = enabled ? "false" : "true" // Inverted: "false" means NOT disabled (i.e., enabled) let gitCommand = "git -C \(brewPrefix) config --replace-all homebrew.analyticsdisabled \(value) 2>&1" let process = Process() process.executableURL = URL(fileURLWithPath: "/bin/sh") process.arguments = ["-c", gitCommand] let pipe = Pipe() process.standardOutput = pipe process.standardError = pipe try process.run() process.waitUntilExit() if process.terminationStatus != 0 { let data = pipe.fileHandleForReading.readDataToEndOfFile() let error = String(data: data, encoding: .utf8) ?? "Unknown error" await MainActor.run { GlobalConsoleManager.shared.appendOutput("Error: \(error)\n", source: CurrentPage.homebrew.title) } throw HomebrewError.commandFailed("Failed to set analytics status: \(error)") } await MainActor.run { GlobalConsoleManager.shared.appendOutput("Analytics status updated successfully\n", source: CurrentPage.homebrew.title) } } func calculateCacheSize() async -> (bytes: Int64, formatted: String) { // Wrapper around runCleanup with dry-run mode // Returns size of cleanable cache without actually deleting anything return try! await runCleanup(dryRun: true) ?? (0, "0 bytes") } func calculateFormulaSize(name: String, version: String) async -> (Int64, String) { // Calculate size of formula installation in Cellar directory // Path format: /opt/homebrew/Cellar// let cellarPath = "\(brewPrefix)/Cellar/\(name)/\(version)" let cellarURL = URL(fileURLWithPath: cellarPath) let fileManager = FileManager.default // Fast path: Try direct version match first (most formulae don't have revisions) var actualCellarURL = cellarURL if !fileManager.fileExists(atPath: cellarPath) { // Fallback: Search for version with revision suffix (e.g., "25.1.0_1") // Also handle HEAD installations (directory named "HEAD-abc1234" but version shows as "2025.10.22") let formulaBasePath = "\(brewPrefix)/Cellar/\(name)" if let versionDirs = try? fileManager.contentsOfDirectory(atPath: formulaBasePath) { // Find directory whose sanitized version matches input version // OR directory that starts with "HEAD" (for HEAD installations) if let matchingDir = versionDirs.first(where: { $0.stripBrewRevisionSuffix() == version || $0.hasPrefix("HEAD") }) { actualCellarURL = URL(fileURLWithPath: "\(formulaBasePath)/\(matchingDir)") } else { return (0, "0 KB") } } else { return (0, "0 KB") } } let totalBytes = totalSizeOnDisk(for: actualCellarURL) // Format as human-readable (must run on main thread - ByteCountFormatter is not thread-safe) let bytesToFormat = totalBytes let formatted = await MainActor.run { ByteCountFormatter.string(fromByteCount: bytesToFormat, countStyle: .file) } return (totalBytes, formatted) } func calculateCaskSize(name: String) async -> (Int64, String) { // Special case for Pearcleaner (running app, not in sortedApps) if name == "pearcleaner" { let pearcleanerPath = URL(fileURLWithPath: "/Applications/Pearcleaner.app") guard FileManager.default.fileExists(atPath: pearcleanerPath.path) else { return (0, "0 KB") } let totalBytes = totalSizeOnDisk(for: pearcleanerPath) let bytesToFormat = totalBytes let formatted = await MainActor.run { ByteCountFormatter.string(fromByteCount: bytesToFormat, countStyle: .file) } return (totalBytes, formatted) } // For casks, get size from AppState.sortedApps (actual installed app) // Must access sortedApps on MainActor let appInfo = await MainActor.run { AppState.shared.sortedApps.first(where: { $0.cask == name }) } if let appInfo = appInfo { // If bundleSize is 0, calculate it now and update the AppInfo if appInfo.bundleSize == 0 { let calculatedSize = totalSizeOnDisk(for: appInfo.path) let formatted = await MainActor.run { ByteCountFormatter.string(fromByteCount: calculatedSize, countStyle: .file) } // Update the AppInfo in sortedApps with calculated size (must access on MainActor) await MainActor.run { if let index = AppState.shared.sortedApps.firstIndex(where: { $0.path == appInfo.path }) { var updatedAppInfo = AppState.shared.sortedApps[index] updatedAppInfo.bundleSize = calculatedSize AppState.shared.sortedApps[index] = updatedAppInfo } } return (calculatedSize, formatted) } // bundleSize is already calculated, just format it let formatted = await MainActor.run { ByteCountFormatter.string(fromByteCount: appInfo.bundleSize, countStyle: .file) } return (appInfo.bundleSize, formatted) } else { // Fallback if app not found in sortedApps - find and calculate from Caskroom path let caskroomPath = "\(brewPrefix)/Caskroom/\(name)" let globPattern = "\(caskroomPath)/*/*.app" // Find the app symlink in Caskroom using glob var globResult = glob_t() defer { globfree(&globResult) } guard glob(globPattern, 0, nil, &globResult) == 0, globResult.gl_pathc > 0, let cPath = globResult.gl_pathv[0], let symlinkPath = String(validatingUTF8: cPath) else { // No .app found - try PKG-only cask fallback return await calculatePKGOnlyCaskSize(caskName: name) } // Resolve symlink to get real path in /Applications let realPath = URL(fileURLWithPath: symlinkPath).resolvingSymlinksInPath() // Calculate size from disk let calculatedSize = totalSizeOnDisk(for: realPath) let formatted = await MainActor.run { ByteCountFormatter.string(fromByteCount: calculatedSize, countStyle: .file) } return (calculatedSize, formatted) } } /// Calculate size for PKG-only casks (no GUI app) by querying PKG receipts /// Used for Java runtimes, drivers, CLI tools, etc. private func calculatePKGOnlyCaskSize(caskName: String) async -> (Int64, String) { let caskroomPath = "\(brewPrefix)/Caskroom/\(caskName)" // Find cask JSON file in .metadata directory let globPattern = "\(caskroomPath)/.metadata/*/*/Casks/\(caskName).json" var globResult = glob_t() defer { globfree(&globResult) } guard glob(globPattern, 0, nil, &globResult) == 0, globResult.gl_pathc > 0, let cPath = globResult.gl_pathv[0], let jsonPath = String(validatingUTF8: cPath) else { // Glob failed (e.g., tap casks) - fallback to Caskroom directory size guard FileManager.default.fileExists(atPath: caskroomPath) else { return (0, "0 KB") } let directorySize = totalSizeOnDisk(for: URL(fileURLWithPath: caskroomPath)) let formatted = await MainActor.run { ByteCountFormatter.string(fromByteCount: directorySize, countStyle: .file) } return (directorySize, formatted) } // Read and parse cask JSON guard let data = try? Data(contentsOf: URL(fileURLWithPath: jsonPath)), let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let artifacts = json["artifacts"] as? [[String: Any]] else { return (0, "0 KB") } // Extract pkgutil identifiers from uninstall directives var pkgIdentifiers: [String] = [] for artifact in artifacts { if let uninstalls = artifact["uninstall"] as? [[String: Any]] { for uninstall in uninstalls { // Handle both string and array formats if let pkgutilString = uninstall["pkgutil"] as? String { pkgIdentifiers.append(pkgutilString) } else if let pkgutilArray = uninstall["pkgutil"] as? [String] { pkgIdentifiers.append(contentsOf: pkgutilArray) } } } } guard !pkgIdentifiers.isEmpty else { // Fallback: Calculate Caskroom directory size directly // This handles casks with custom uninstall scripts but no pkgutil directive (e.g., fuse-t) guard FileManager.default.fileExists(atPath: caskroomPath) else { return (0, "0 KB") } let directorySize = totalSizeOnDisk(for: URL(fileURLWithPath: caskroomPath)) let formatted = await MainActor.run { ByteCountFormatter.string(fromByteCount: directorySize, countStyle: .file) } return (directorySize, formatted) } // Query PKG receipts and sum sizes let receipts = PKGManager.getAllPackages() var totalSize: Int64 = 0 for identifier in pkgIdentifiers { if let receipt = receipts.first(where: { ($0.packageIdentifier() as? String) == identifier }), let bomInfo = PKGManager.getBOMInfo(for: receipt) { totalSize += bomInfo.totalSize } } guard totalSize > 0 else { return (0, "0 KB") } let bytesFormatted = totalSize let formatted = await MainActor.run { ByteCountFormatter.string(fromByteCount: bytesFormatted, countStyle: .file) } return (totalSize, formatted) } // MARK: - Tap Package Loading func getPackagesFromTap(_ tapName: String) async throws -> (formulae: [String], casks: [String]) { let tapPath = "\(brewPrefix)/Library/Taps/\(tapName.replacingOccurrences(of: "/", with: "/homebrew-"))" var formulae: [String] = [] var casks: [String] = [] // Load formulae - read directly from filesystem let formulaPath = "\(tapPath)/Formula" if FileManager.default.fileExists(atPath: formulaPath) { if let files = try? FileManager.default.contentsOfDirectory(atPath: formulaPath) { for file in files where file.hasSuffix(".rb") { let name = file.replacingOccurrences(of: ".rb", with: "") formulae.append(name) } } } // Load casks - read directly from filesystem (recursively, since they're nested in letter directories) let caskPath = "\(tapPath)/Casks" if FileManager.default.fileExists(atPath: caskPath) { let caskFiles = try recursivelyFindCasks(in: caskPath) for file in caskFiles { let name = file.replacingOccurrences(of: ".rb", with: "") casks.append(name) } } // Sort alphabetically formulae.sort() casks.sort() return (formulae, casks) } // Helper to recursively find cask files private func recursivelyFindCasks(in directory: String) throws -> [String] { var caskNames: [String] = [] guard let enumerator = FileManager.default.enumerator(atPath: directory) else { return [] } for case let file as String in enumerator { if file.hasSuffix(".rb") { // Remove .rb extension and any parent directories (like "b/") let name = file.replacingOccurrences(of: ".rb", with: "") .components(separatedBy: "/") .last ?? file.replacingOccurrences(of: ".rb", with: "") caskNames.append(name) } } return caskNames } // Get full package info from brew info private func getPackageDetailsFromBrew(fullName: String, cask: Bool) async throws -> HomebrewSearchResult? { let arguments = ["info", "--json=v2", fullName] let result = try await runBrewCommand(arguments) guard let jsonData = result.output.data(using: .utf8), let json = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any] else { return nil } let array = cask ? (json["casks"] as? [[String: Any]]) : (json["formulae"] as? [[String: Any]]) guard let item = array?.first else { return nil } let name = cask ? (item["full_token"] as? String ?? "") : (item["full_name"] as? String ?? "") let desc = item["desc"] as? String let homepage = item["homepage"] as? String let license = item["license"] as? String let version: String? = cask ? (item["version"] as? String) : ((item["versions"] as? [String: Any])?["stable"] as? String) let dependencies: [String] if cask { let dependsOn = item["depends_on"] as? [String: Any] dependencies = (dependsOn?["formula"] as? [String]) ?? [] } else { dependencies = (item["dependencies"] as? [String]) ?? [] } let caveats = item["caveats"] as? String // Common fields let tap = item["tap"] as? String let _ = item["full_name"] as? String // Unused, but keep for consistency let isDeprecated = (item["deprecated"] as? Bool) ?? false let deprecationReason = item["deprecation_reason"] as? String let isDisabled = (item["disabled"] as? Bool) ?? false let disableDate = item["disable_date"] as? String let conflictsWith = (item["conflicts_with"] as? [String]) ?? [] let conflictsWithReasons = (item["conflicts_with_reasons"] as? [String]) ?? [] // Formula-specific fields let isBottled = cask ? nil : ((item["versions"] as? [String: Any])?["bottle"] as? Bool ?? false) let isKegOnly = cask ? nil : (item["keg_only"] as? Bool ?? false) let kegOnlyReason: String? if !cask { if let kegOnlyReasonDict = item["keg_only_reason"] as? [String: Any], let explanation = kegOnlyReasonDict["explanation"] as? String, !explanation.isEmpty { kegOnlyReason = explanation } else if let kegOnlyReasonDict = item["keg_only_reason"] as? [String: Any], let reason = kegOnlyReasonDict["reason"] as? String { switch reason { case ":provided_by_macos": kegOnlyReason = "macOS already provides this software" case ":versioned_formula": kegOnlyReason = "This is a versioned formula" case ":shadowed_by_macos": kegOnlyReason = "Shadowed by macOS" default: kegOnlyReason = "Not symlinked to Homebrew prefix" } } else { kegOnlyReason = nil } } else { kegOnlyReason = nil } let buildDependencies = cask ? nil : (item["build_dependencies"] as? [String]) let aliases = cask ? nil : (item["aliases"] as? [String]) let versionedFormulae = cask ? nil : (item["versioned_formulae"] as? [String]) let requirements: String? if !cask { requirements = (item["requirements"] as? [[String: Any]])?.compactMap { req in if req["name"] as? String == "macos", let version = req["version"] as? String { return "macOS >= \(version)" } return nil }.first } else { requirements = nil } // Cask-specific fields let caskName = cask ? (item["name"] as? [String]) : nil let autoUpdates = cask ? (item["auto_updates"] as? Bool) : nil let artifacts = cask ? (item["artifacts"] as? [[String: Any]])?.compactMap { artifact -> String? in if let appArray = artifact["app"] as? [String], let app = appArray.first { return "\(app) (App)" } else if let pkgArray = artifact["pkg"] as? [String], let pkg = pkgArray.first { return "\(pkg) (Pkg)" } return nil } : nil return HomebrewSearchResult( name: name, displayName: nil, // Not loaded for tap packages description: desc, homepage: homepage, license: license, version: version, dependencies: dependencies.isEmpty ? nil : dependencies, caveats: caveats, tap: tap, fullName: fullName, isDeprecated: isDeprecated, deprecationReason: deprecationReason, deprecationDate: nil, // Not available in JWS cache isDisabled: isDisabled, disableDate: disableDate, disableReason: nil, // Not available in JWS cache conflictsWith: conflictsWith.isEmpty ? nil : conflictsWith, conflictsWithReasons: conflictsWithReasons.isEmpty ? nil : conflictsWithReasons, isBottled: isBottled, isKegOnly: isKegOnly, kegOnlyReason: kegOnlyReason, buildDependencies: buildDependencies, optionalDependencies: nil, // Not available in JWS cache recommendedDependencies: nil, // Not available in JWS cache usesFromMacos: nil, // Not available in JWS cache aliases: aliases, versionedFormulae: versionedFormulae, requirements: requirements, caskName: caskName, autoUpdates: autoUpdates, artifacts: artifacts, url: nil, // Not available in JWS cache appcast: nil // Not available in JWS cache ) } } ================================================ FILE: Pearcleaner/Logic/Brew/HomebrewManager.swift ================================================ // // HomebrewManager.swift // Pearcleaner // // Created by Alin Lupascu on 10/01/25. // import Foundation import AlinFoundation enum InstalledCategory: String, CaseIterable { case outdated = "Outdated" case formulae = "Formulae" case casks = "Casks" var icon: String { switch self { case .outdated: return "arrow.triangle.2.circlepath" case .formulae: return "terminal" case .casks: return "shippingbox" } } } enum AvailableCategory: String, CaseIterable { case formulae = "Formulae" case casks = "Casks" var icon: String { switch self { case .formulae: return "terminal" case .casks: return "shippingbox" } } } @MainActor class HomebrewManager: ObservableObject { // Lightweight models for Browse tab Installed section (fast streaming) @Published var installedFormulae: [InstalledPackage] = [] @Published var installedCasks: [InstalledPackage] = [] @Published var outdatedPackagesMap: [String: OutdatedVersionInfo] = [:] @Published var isLoadingOutdated: Bool = false // Organized categories for Browse tab (matches UpdateManager pattern) @Published var installedByCategory: [InstalledCategory: [HomebrewSearchResult]] = [:] @Published var availableByCategory: [AvailableCategory: [HomebrewSearchResult]] = [:] @Published var availableTaps: [HomebrewTapInfo] = [] @Published var isLoadingPackages: Bool = false @Published var isLoadingTaps: Bool = false @Published var brewVersion: String = "" @Published var latestBrewVersion: String = "" @Published var updateAvailable: Bool = false @Published var cacheSize: Int64 = 0 // Swift-calculated cache size (instant) @Published var analyticsEnabled: Bool = false @Published var isCheckingAnalytics: Bool = false // Browse tab cached packages @Published var allAvailableFormulae: [HomebrewSearchResult] = [] @Published var allAvailableCasks: [HomebrewSearchResult] = [] // Track if initial data has been loaded for session var hasLoadedInstalledPackages: Bool = false var hasLoadedAvailablePackages: Bool = false @Published var isLoadingAvailablePackages: Bool = false // Maintenance tab refresh trigger @Published var maintenanceRefreshTrigger: Bool = false var allPackages: [InstalledPackage] { return installedFormulae + installedCasks } // Calculate outdated packages by comparing installed version with JWS version var outdatedPackages: [InstalledPackage] { return allPackages.filter { package in // Use brew outdated as source of truth return outdatedPackagesMap.keys.contains(package.name) } } /// Get version information for an outdated package /// Returns version info or nil if not outdated func getOutdatedVersions(for packageName: String) -> OutdatedVersionInfo? { // Try exact name first if let versions = outdatedPackagesMap[packageName] { return versions } // Try short name fallback (e.g., "homebrew/cask/name" -> "name") let shortName = packageName.components(separatedBy: "/").last ?? packageName return outdatedPackagesMap[shortName] } func refreshAll() async { async let packages: Void = loadInstalledPackages() async let taps: Void = loadTaps() async let version: Void = loadBrewVersion() async let cache: Void = loadCacheSize() async let analytics: Void = checkAnalyticsStatus() _ = await (packages, taps, version, cache, analytics) } func refreshMaintenance() async { // Toggle trigger immediately to notify MaintenanceSection to re-run checks maintenanceRefreshTrigger.toggle() async let version: Void = loadBrewVersion() async let cache: Void = loadCacheSize() async let analytics: Void = checkAnalyticsStatus() async let update: Void = checkForUpdate() _ = await (version, cache, analytics, update) } func loadInstalledPackages() async { isLoadingPackages = true defer { isLoadingPackages = false } // Clear existing arrays installedFormulae.removeAll() installedCasks.removeAll() // Temporary arrays to collect all packages before updating @Published properties var tempFormulae: [InstalledPackage] = [] var tempCasks: [InstalledPackage] = [] do { // Fast scanner - reads local files directly (~70ms total) // Collect formulae with installedOnRequest from INSTALL_RECEIPT.json try await HomebrewController.shared.streamInstalledPackages(cask: false) { name, displayName, desc, version, isPinned, tap, tapRbPath, installedOnRequest in let package = InstalledPackage( name: name, displayName: displayName, description: desc, version: version, isCask: false, isPinned: isPinned, tap: tap, tapRbPath: tapRbPath, installedOnRequest: installedOnRequest ) tempFormulae.append(package) } // Collect casks (casks are always installed on request) try await HomebrewController.shared.streamInstalledPackages(cask: true) { name, displayName, desc, version, isPinned, tap, tapRbPath, installedOnRequest in let package = InstalledPackage( name: name, displayName: displayName, description: desc, version: version, isCask: true, isPinned: isPinned, tap: tap, tapRbPath: tapRbPath, installedOnRequest: installedOnRequest // Always true for casks ) tempCasks.append(package) } // Update @Published properties once with all packages (sorted alphabetically by display name) installedFormulae = tempFormulae.sorted { ($0.displayName ?? $0.name).sortKey < ($1.displayName ?? $1.name).sortKey } installedCasks = tempCasks.sorted { ($0.displayName ?? $0.name).sortKey < ($1.displayName ?? $1.name).sortKey } // Mark as loaded for this session hasLoadedInstalledPackages = true // Populate installedByCategory immediately with initial data (empty outdated for now) updateInstalledCategories() // Load outdated packages using hybrid approach (don't block UI) // ~3.5x faster than `brew outdated` for core packages, accurate for tap packages Task { await MainActor.run { isLoadingOutdated = true } let packages = await HomebrewController.shared.getOutdatedPackagesHybrid( formulae: tempFormulae, casks: tempCasks ) await MainActor.run { outdatedPackagesMap = Dictionary(uniqueKeysWithValues: packages.map { // Strip revision suffix for casks (Sparkle updates), keep for formulae (revision tracking) let installedDisplay = $0.isCask ? $0.installedVersion.stripBrewRevisionSuffix() : $0.installedVersion let availableDisplay = $0.isCask ? $0.availableVersion.stripBrewRevisionSuffix() : $0.availableVersion return ($0.name, OutdatedVersionInfo(installed: installedDisplay, available: availableDisplay)) }) isLoadingOutdated = false // Update categories now that we have outdated info updateInstalledCategories() // Print debug log report if debug logging is enabled if UserDefaults.standard.object(forKey: "settings.updater.debugLogging") as? Bool ?? true { printOS("\n" + UpdaterDebugLogger.shared.generateDebugReport()) } } } } catch { printOS("Error loading packages: \(error)") } } /// Refresh only specific packages after install/update/uninstall operations /// Much faster than full loadInstalledPackages() scan - only checks specified packages func refreshSpecificPackages(_ packageNames: [String]) async { guard !packageNames.isEmpty else { return } UpdaterDebugLogger.shared.log(.homebrew, "🔄 Refreshing \(packageNames.count) specific package(s): \(packageNames.joined(separator: ", "))") isLoadingPackages = true defer { isLoadingPackages = false } for name in packageNames { // Check both Cellar (formulae) and Caskroom (casks) let cellarPath = "\(HomebrewController.shared.brewPrefix)/Cellar/\(name)" let caskroomPath = "\(HomebrewController.shared.brewPrefix)/Caskroom/\(name)" if FileManager.default.fileExists(atPath: cellarPath) { // Formula still installed - update its info UpdaterDebugLogger.shared.log(.homebrew, " Updating formula: \(name)") await refreshSinglePackage(name: name, isCask: false) } else if FileManager.default.fileExists(atPath: caskroomPath) { // Cask still installed - update its info UpdaterDebugLogger.shared.log(.homebrew, " Updating cask: \(name)") await refreshSinglePackage(name: name, isCask: true) } else { // Package uninstalled - remove from lists UpdaterDebugLogger.shared.log(.homebrew, " Package removed: \(name)") installedFormulae.removeAll { $0.name == name } installedCasks.removeAll { $0.name == name } } } // Update categories with refreshed data updateInstalledCategories() // Check outdated status for these specific packages only await refreshOutdatedStatus(for: packageNames) UpdaterDebugLogger.shared.log(.homebrew, "✓ Refresh complete for \(packageNames.count) package(s)") } /// Refresh a single package's information from Cellar/Caskroom private func refreshSinglePackage(name: String, isCask: Bool) async { do { var updatedPackage: InstalledPackage? // Stream only this specific package try await HomebrewController.shared.streamInstalledPackages(cask: isCask) { pkgName, displayName, desc, version, isPinned, tap, tapRbPath, installedOnRequest in if pkgName == name { updatedPackage = InstalledPackage( name: pkgName, displayName: displayName, description: desc, version: version, isCask: isCask, isPinned: isPinned, tap: tap, tapRbPath: tapRbPath, installedOnRequest: installedOnRequest ) } } // Update the appropriate array if let updated = updatedPackage { if isCask { if let index = installedCasks.firstIndex(where: { $0.name == name }) { installedCasks[index] = updated } else { installedCasks.append(updated) } // Re-sort after adding/updating to maintain alphabetical order installedCasks.sort { ($0.displayName ?? $0.name).sortKey < ($1.displayName ?? $1.name).sortKey } } else { if let index = installedFormulae.firstIndex(where: { $0.name == name }) { installedFormulae[index] = updated } else { installedFormulae.append(updated) } // Re-sort after adding/updating to maintain alphabetical order installedFormulae.sort { ($0.displayName ?? $0.name).sortKey < ($1.displayName ?? $1.name).sortKey } } } } catch { printOS("Error refreshing package \(name): \(error)") } } /// Check outdated status for specific packages only (much faster than checking all) private func refreshOutdatedStatus(for packageNames: [String]) async { isLoadingOutdated = true defer { isLoadingOutdated = false } // Get packages to check let packagesToCheck = allPackages.filter { packageNames.contains($0.name) } // Check outdated status using hybrid approach (only for these specific packages) let updatedOutdated = await HomebrewController.shared.getOutdatedPackagesHybrid( formulae: packagesToCheck.filter { !$0.isCask }, casks: packagesToCheck.filter { $0.isCask } ) // Update outdatedPackagesMap - remove old entries for these packages, add new ones if outdated for name in packageNames { outdatedPackagesMap.removeValue(forKey: name) } for package in updatedOutdated { // Strip revision suffix for casks (Sparkle updates), keep for formulae (revision tracking) let installedDisplay = package.isCask ? package.installedVersion.stripBrewRevisionSuffix() : package.installedVersion let availableDisplay = package.isCask ? package.availableVersion.stripBrewRevisionSuffix() : package.availableVersion outdatedPackagesMap[package.name] = OutdatedVersionInfo( installed: installedDisplay, available: availableDisplay ) } // Update categories with new outdated status updateInstalledCategories() } // Update the installedByCategory dictionary (matches UpdateManager pattern) func updateInstalledCategories() { let allPackages = installedFormulae + installedCasks let allConverted = allPackages.map { convertToSearchResult($0) } // Separate into categories var outdated: [HomebrewSearchResult] = [] var formulae: [HomebrewSearchResult] = [] var casks: [HomebrewSearchResult] = [] for result in allConverted { let isCask = installedCasks.contains(where: { $0.name == result.name }) // Add to type-based category if isCask { casks.append(result) } else { formulae.append(result) } // Also add to Outdated if outdated if isPackageOutdated(result) { outdated.append(result) } } // Sort and update dictionary (by displayName for consistent alphabetical ordering) installedByCategory[.outdated] = outdated.sorted { ($0.displayName ?? $0.name).sortKey < ($1.displayName ?? $1.name).sortKey } installedByCategory[.formulae] = formulae.sorted { ($0.displayName ?? $0.name).sortKey < ($1.displayName ?? $1.name).sortKey } installedByCategory[.casks] = casks.sorted { ($0.displayName ?? $0.name).sortKey < ($1.displayName ?? $1.name).sortKey } } private func isPackageOutdated(_ result: HomebrewSearchResult) -> Bool { let shortName = result.name.components(separatedBy: "/").last ?? result.name return outdatedPackagesMap.keys.contains(result.name) || outdatedPackagesMap.keys.contains(shortName) } private func convertToSearchResult(_ package: InstalledPackage) -> HomebrewSearchResult { // Look up tap info from available packages let availablePackages = package.isCask ? allAvailableCasks : allAvailableFormulae let shortName = package.name.components(separatedBy: "/").last ?? package.name // Try multiple matching strategies let matchingPackage = availablePackages.first(where: { if $0.name == package.name { return true } if $0.name == shortName { return true } let availableShortName = $0.name.components(separatedBy: "/").last ?? $0.name return availableShortName == shortName }) let tap = package.tap // For installed casks, prefer AppInfo name over Homebrew displayName for consistent sorting let finalDisplayName: String? = { if package.isCask { // Look up app in sortedApps to get actual app name if let appInfo = AppState.shared.sortedApps.first(where: { $0.cask == package.name || $0.cask == shortName }) { return appInfo.appName // Use AppInfo name (e.g., "AppCleaner") } } // Fallback: Ruby file → JWS lookup return package.displayName ?? matchingPackage?.displayName }() return HomebrewSearchResult( name: package.name, displayName: finalDisplayName, description: package.description, homepage: nil, license: nil, version: package.version, dependencies: nil, caveats: nil, tap: tap, fullName: nil, isDeprecated: false, deprecationReason: nil, deprecationDate: nil, isDisabled: false, disableDate: nil, disableReason: nil, conflictsWith: nil, conflictsWithReasons: nil, isBottled: nil, isKegOnly: nil, kegOnlyReason: nil, buildDependencies: nil, optionalDependencies: nil, recommendedDependencies: nil, usesFromMacos: nil, aliases: nil, versionedFormulae: nil, requirements: nil, caskName: nil, autoUpdates: nil, artifacts: nil, url: nil, appcast: nil ) } func loadTaps() async { isLoadingTaps = true defer { isLoadingTaps = false } do { availableTaps = try await HomebrewController.shared.loadTaps() } catch { printOS("Error loading taps: \(error)") } } func removeTapFromList(name: String) { availableTaps.removeAll { $0.name == name } } func loadBrewVersion() async { do { brewVersion = try await HomebrewController.shared.getBrewVersion() } catch { printOS("Error loading brew version: \(error)") } } func checkForUpdate() async { do { let result = try await HomebrewController.shared.checkForBrewUpdate() brewVersion = result.current latestBrewVersion = result.latest updateAvailable = result.updateAvailable } catch { printOS("Error checking for brew update: \(error)") } } func loadCacheSize() async { let result = await HomebrewController.shared.calculateCacheSize() cacheSize = result.bytes } func checkAnalyticsStatus() async { isCheckingAnalytics = true defer { isCheckingAnalytics = false } do { analyticsEnabled = try await HomebrewController.shared.getAnalyticsStatus() } catch { printOS("Error checking analytics status: \(error)") } } func loadAvailablePackages(appState: AppState, forceRefresh: Bool = false) async { guard forceRefresh || (allAvailableFormulae.isEmpty && allAvailableCasks.isEmpty) else { hasLoadedAvailablePackages = true return } isLoadingAvailablePackages = true defer { isLoadingAvailablePackages = false } do { // Update Homebrew first to ensure JWS files are up to date if forceRefresh { try await HomebrewController.shared.updateBrew() } // Load both JWS files in parallel (~0.63s total from earlier test) async let formulaeMetadata = HomebrewController.shared.loadMinimalPackageMetadata(cask: false) async let casksMetadata = HomebrewController.shared.loadMinimalPackageMetadata(cask: true) let (formulae, casks) = try await (formulaeMetadata, casksMetadata) // Convert to SearchResult objects with displayName, description, and version // Sort once here to avoid sorting on every search keystroke allAvailableFormulae = formulae.map { (name, displayName, description, version, _) in HomebrewSearchResult( name: name, displayName: displayName, description: description, homepage: nil, license: nil, version: version, dependencies: nil, caveats: nil, tap: nil, fullName: nil, isDeprecated: false, deprecationReason: nil, deprecationDate: nil, isDisabled: false, disableDate: nil, disableReason: nil, conflictsWith: nil, conflictsWithReasons: nil, isBottled: nil, isKegOnly: nil, kegOnlyReason: nil, buildDependencies: nil, optionalDependencies: nil, recommendedDependencies: nil, usesFromMacos: nil, aliases: nil, versionedFormulae: nil, requirements: nil, caskName: nil, autoUpdates: nil, artifacts: nil, url: nil, appcast: nil ) }.sorted { ($0.displayName ?? $0.name).sortKey < ($1.displayName ?? $1.name).sortKey } allAvailableCasks = casks.map { (name, displayName, description, version, _) in HomebrewSearchResult( name: name, displayName: displayName, description: description, homepage: nil, license: nil, version: version, dependencies: nil, caveats: nil, tap: nil, fullName: nil, isDeprecated: false, deprecationReason: nil, deprecationDate: nil, isDisabled: false, disableDate: nil, disableReason: nil, conflictsWith: nil, conflictsWithReasons: nil, isBottled: nil, isKegOnly: nil, kegOnlyReason: nil, buildDependencies: nil, optionalDependencies: nil, recommendedDependencies: nil, usesFromMacos: nil, aliases: nil, versionedFormulae: nil, requirements: nil, caskName: nil, autoUpdates: nil, artifacts: nil, url: nil, appcast: nil ) }.sorted { ($0.displayName ?? $0.name).sortKey < ($1.displayName ?? $1.name).sortKey } // Populate availableByCategory dictionary availableByCategory[.formulae] = allAvailableFormulae availableByCategory[.casks] = allAvailableCasks hasLoadedAvailablePackages = true } catch { printOS("Error loading package metadata: \(error)") } } } ================================================ FILE: Pearcleaner/Logic/Brew/HomebrewPackage.swift ================================================ // // HomebrewPackage.swift // Pearcleaner // // Created by Alin Lupascu on 10/01/25. // import Foundation // MARK: - Legacy Model (for search/list views) struct HomebrewSearchResult: Identifiable, Hashable { let id = UUID() let name: String let displayName: String? // Human-readable app name (e.g., "FreeMacSoft AppCleaner" instead of "appcleaner") let description: String? let homepage: String? let license: String? let version: String? let dependencies: [String]? let caveats: String? // Common fields (from JWS) let tap: String? let fullName: String? let isDeprecated: Bool let deprecationReason: String? let deprecationDate: String? let isDisabled: Bool let disableDate: String? let disableReason: String? let conflictsWith: [String]? let conflictsWithReasons: [String]? // Formula-specific fields (from JWS) let isBottled: Bool? let isKegOnly: Bool? let kegOnlyReason: String? let buildDependencies: [String]? let optionalDependencies: [String]? let recommendedDependencies: [String]? let usesFromMacos: [String]? let aliases: [String]? let versionedFormulae: [String]? let requirements: String? // Cask-specific fields (from JWS) let caskName: [String]? let autoUpdates: Bool? let artifacts: [String]? let url: String? let appcast: String? } // MARK: - Type-Safe Package Details Models /// Enum wrapper for type-safe package details enum PackageDetailsType { case formula(FormulaDetails) case cask(CaskDetails) var name: String { switch self { case .formula(let details): return details.name case .cask(let details): return details.name } } var isCask: Bool { switch self { case .formula: return false case .cask: return true } } } /// Base protocol with fields common to both formulae and casks protocol HomebrewPackageDetails { var name: String { get } var description: String? { get } var homepage: String? { get } var license: String? { get } var version: String? { get } var dependencies: [String]? { get } var caveats: String? { get } var tap: String? { get } var fullName: String? { get } var isDeprecated: Bool { get } var deprecationReason: String? { get } var deprecationDate: String? { get } var isDisabled: Bool { get } var disableDate: String? { get } var disableReason: String? { get } var conflictsWith: [String]? { get } var conflictsWithReasons: [String]? { get } } /// Formula-specific package details struct FormulaDetails: HomebrewPackageDetails { // Common fields let name: String let description: String? let homepage: String? let license: String? let version: String? let dependencies: [String]? let caveats: String? let tap: String? let fullName: String? let isDeprecated: Bool let deprecationReason: String? let deprecationDate: String? let isDisabled: Bool let disableDate: String? let disableReason: String? let conflictsWith: [String]? let conflictsWithReasons: [String]? // Formula-specific fields let isBottled: Bool? let isKegOnly: Bool? let kegOnlyReason: String? let buildDependencies: [String]? let optionalDependencies: [String]? let recommendedDependencies: [String]? let usesFromMacos: [String]? let aliases: [String]? let versionedFormulae: [String]? let requirements: String? let service: ServiceInfo? // Replacement suggestions let deprecationReplacementFormula: String? let deprecationReplacementCask: String? let disableReplacementFormula: String? let disableReplacementCask: String? } /// Cask-specific package details struct CaskDetails: HomebrewPackageDetails { // Common fields let name: String let description: String? let homepage: String? let license: String? let version: String? let dependencies: [String]? let caveats: String? let tap: String? let fullName: String? let isDeprecated: Bool let deprecationReason: String? let deprecationDate: String? let isDisabled: Bool let disableDate: String? let disableReason: String? let conflictsWith: [String]? let conflictsWithReasons: [String]? // Cask-specific fields let caskName: [String]? let autoUpdates: Bool? let artifacts: [String]? let url: String? let appcast: String? let minimumMacOSVersion: String? let architectureRequirement: ArchRequirement? let bundleVersion: String? // CFBundleVersion from API (e.g., "7390.122" or "446000104") let bundleShortVersion: String? // CFBundleShortVersionString from API // Replacement suggestions let deprecationReplacementFormula: String? let deprecationReplacementCask: String? let disableReplacementFormula: String? let disableReplacementCask: String? } /// Service/daemon information for formulae struct ServiceInfo { let run: [String]? // Command array let runType: String? // immediate, interval, cron, etc. let workingDir: String? let keepAlive: Bool? } /// Architecture requirement for casks enum ArchRequirement: String { case intel = "x86_64" case arm = "arm64" case universal = "universal" var displayName: String { switch self { case .intel: return "Intel only" case .arm: return "Apple Silicon only" case .universal: return "Universal (Intel & Apple Silicon)" } } } struct HomebrewAnalytics { let install30d: Int? let install90d: Int? let install365d: Int? } // Lightweight model for installed packages list (only name + description + version displayed) struct OutdatedVersionInfo: Equatable, Hashable { let installed: String let available: String } struct InstalledPackage: Identifiable, Equatable, Hashable { let id = UUID() let name: String let displayName: String? // Human-readable app name from Ruby file (e.g., "Battery Toolkit" for casks, nil for formulae) let description: String? let version: String? let isCask: Bool var isPinned: Bool let tap: String? // e.g., "homebrew/core", "mhaeuser/mhaeuser" let tapRbPath: String? // Cached path to tap's .rb file for version checking let installedOnRequest: Bool // True if user explicitly installed (not as dependency) - formulae only, always true for casks var size: String? // Human-readable size (e.g., "15 MB") - calculated lazily var sizeBytes: Int64? // Size in bytes for sorting - calculated lazily static func == (lhs: InstalledPackage, rhs: InstalledPackage) -> Bool { return lhs.name == rhs.name && lhs.isCask == rhs.isCask } func hash(into hasher: inout Hasher) { hasher.combine(name) hasher.combine(isCask) } } struct HomebrewPackageInfo: Identifiable, Equatable, Hashable { let id = UUID() let name: String let isCask: Bool let installedOn: Date? let versions: [String] let sizeInBytes: Int64? let isPinned: Bool let isOutdated: Bool let description: String? let homepage: String? let tap: String? let installedPath: String? // Cellar path for formulae, Caskroom path for casks let fileCount: Int? // Number of files in installation var displayVersion: String { return versions.joined(separator: ", ") } func formattedInstallDate(date: Date) -> String { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "d MMM y (E), HH:mm" return dateFormatter.string(from: date) } func formattedSize(size: Int64) -> String { let formatter = ByteCountFormatter() formatter.allowsNonnumericFormatting = false formatter.countStyle = .file return formatter.string(fromByteCount: size) } static func == (lhs: HomebrewPackageInfo, rhs: HomebrewPackageInfo) -> Bool { return lhs.name == rhs.name && lhs.isCask == rhs.isCask } func hash(into hasher: inout Hasher) { hasher.combine(name) hasher.combine(isCask) } // Full initializer (for backward compatibility with old loadInstalledPackages) init( name: String, isCask: Bool, installedOn: Date?, versions: [String], sizeInBytes: Int64?, isPinned: Bool, isOutdated: Bool, description: String?, homepage: String?, tap: String?, installedPath: String?, fileCount: Int? ) { self.name = name self.isCask = isCask self.installedOn = installedOn self.versions = versions self.sizeInBytes = sizeInBytes self.isPinned = isPinned self.isOutdated = isOutdated self.description = description self.homepage = homepage self.tap = tap self.installedPath = installedPath self.fileCount = fileCount } } ================================================ FILE: Pearcleaner/Logic/Brew/HomebrewTap.swift ================================================ // // HomebrewTap.swift // Pearcleaner // // Created by Alin Lupascu on 10/01/25. // import Foundation struct HomebrewTapInfo: Identifiable, Equatable, Hashable { let id = UUID() let name: String let isOfficial: Bool var displayName: String { return name } static func == (lhs: HomebrewTapInfo, rhs: HomebrewTapInfo) -> Bool { return lhs.name == rhs.name } func hash(into hasher: inout Hasher) { hasher.combine(name) } } ================================================ FILE: Pearcleaner/Logic/Brew/HomebrewUninstaller.swift ================================================ // // HomebrewUninstaller.swift // Pearcleaner // // Created by Pearcleaner on 2025-10-03. // import Foundation import AlinFoundation import ServiceManagement class HomebrewUninstaller { static let shared = HomebrewUninstaller() private var brewPrefix: String { HomebrewController.shared.brewPrefix } private let useBrewUninstallZap = true // Set to true to use brew command, false for manual method private init() {} // MARK: - Main Entry Point /// Uninstalls a Homebrew package directly without calling brew uninstall /// This replicates Homebrew's uninstall behavior using privileged helper for root operations func uninstallPackage(name: String, cask: Bool, zap: Bool = true) async throws { UpdaterDebugLogger.shared.log(.homebrew, "🗑️ Starting uninstall for \(name) (type: \(cask ? "cask" : "formula"), zap: \(zap))") do { if useBrewUninstallZap { // Use native brew uninstall command try await uninstallViaBrewCommand(name: name, cask: cask) } else { // Use manual uninstall method if cask { // Try loading from INSTALL_RECEIPT.json first (instant) let caskInfo: [String: Any] do { caskInfo = try loadCaskInfoFromReceipt(name: name) UpdaterDebugLogger.shared.log(.homebrew, " Loaded cask info from INSTALL_RECEIPT.json") } catch { // Fallback to brew info command (slower but works if receipt missing) UpdaterDebugLogger.shared.log(.homebrew, " INSTALL_RECEIPT.json not found, falling back to brew info") let arguments = ["info", "--json=v2", name] let result = try await HomebrewController.shared.runBrewCommand(arguments) guard let jsonData = result.output.data(using: String.Encoding.utf8) else { throw HomebrewError.jsonParseError } guard let json = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any], let casks = json["casks"] as? [[String: Any]], let info = casks.first else { throw HomebrewError.commandFailed("Cask \(name) not found") } caskInfo = info } try await uninstallCask(name: name, info: caskInfo, zap: zap) } else { // Formulae don't need info JSON - brew uninstall handles everything try await uninstallFormula(name: name, info: [:]) } } UpdaterDebugLogger.shared.log(.homebrew, "✓ Uninstalled \(name) successfully") // Run brew cleanup synchronously (FilesView manages the progress indicator) UpdaterDebugLogger.shared.log(.homebrew, " Running cleanup...") _ = try? await HomebrewController.shared.runCleanup() } catch { UpdaterDebugLogger.shared.log(.homebrew, "❌ Uninstall failed for \(name): \(error.localizedDescription)") throw error } } // MARK: - Brew Command Method /// Uninstalls a package using native brew uninstall command private func uninstallViaBrewCommand(name: String, cask: Bool) async throws { var arguments = ["uninstall"] // Add package type flag if cask { arguments.append("--cask") arguments.append("--zap") } else { arguments.append("--formula") } // Force uninstall arguments.append("--force") // Add package name arguments.append(name) UpdaterDebugLogger.shared.log(.homebrew, " Running: brew \(arguments.joined(separator: " "))") // Run command let result = try await HomebrewController.shared.runBrewCommand(arguments) // Print full stdout and stderr // if !result.output.isEmpty { // printOS("📤 STDOUT:\n\(result.output)") // } if !result.error.isEmpty { printOS("📤 Homebrew Uninstall Error:\n\(result.error)") } // Check for errors if !result.error.isEmpty && result.error.contains("Error") { // Parse specific errors if let depError = parseDependencyConflict(from: result.error, package: name) { throw depError } // Fallback to generic error throw HomebrewError.commandFailed(result.error) } } // MARK: - INSTALL_RECEIPT Helper /// Load cask info from INSTALL_RECEIPT.json (instant, no brew command needed) /// Converts receipt format to brew info format for compatibility with uninstallCask() private func loadCaskInfoFromReceipt(name: String) throws -> [String: Any] { let receiptPath = "\(brewPrefix)/Caskroom/\(name)/.metadata/INSTALL_RECEIPT.json" guard FileManager.default.fileExists(atPath: receiptPath) else { throw HomebrewError.commandFailed("INSTALL_RECEIPT.json not found for \(name)") } let data = try Data(contentsOf: URL(fileURLWithPath: receiptPath)) guard let receipt = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { throw HomebrewError.jsonParseError } // Convert INSTALL_RECEIPT format to brew info format var caskInfo: [String: Any] = [:] caskInfo["token"] = name caskInfo["artifacts"] = receipt["uninstall_artifacts"] return caskInfo } // MARK: - Cask Uninstall private func uninstallCask(name: String, info: [String: Any], zap: Bool) async throws { let token = info["token"] as? String ?? name var filesToDelete: [URL] = [] // Collect all process names and services to kill var processNamesToKill: Set = [] var serviceNamesToKill: Set = [] var appName: String? // Process artifacts (includes uninstall directives and app info) if let artifacts = info["artifacts"] as? [[String: Any]] { // Collect app name for process killing for artifact in artifacts { if let appArray = artifact["app"] as? [String], let app = appArray.first { appName = app.replacingOccurrences(of: ".app", with: "") processNamesToKill.insert(appName!) } } // Collect quit and launchctl names from uninstall directives for artifact in artifacts { if let uninstallDirectives = artifact["uninstall"] as? [[String: Any]] { for directive in uninstallDirectives { if let quit = directive["quit"] as? String { processNamesToKill.insert(quit) } if let launchctl = directive["launchctl"] as? String { serviceNamesToKill.insert(launchctl) } } } } // First pass: Process service/script directives and collect file paths for artifact in artifacts { if let uninstallDirectives = artifact["uninstall"] as? [[String: Any]], !uninstallDirectives.isEmpty { let files = try await processCaskUninstallDirectives(uninstallDirectives, caskName: token, allProcessNames: processNamesToKill, allServiceNames: serviceNamesToKill) filesToDelete.append(contentsOf: files) } } // Process zap directives if requested if zap { for artifact in artifacts { if let zapDirectives = artifact["zap"] as? [[String: Any]], !zapDirectives.isEmpty { let files = try await processCaskUninstallDirectives(zapDirectives, caskName: token, allProcessNames: processNamesToKill, allServiceNames: serviceNamesToKill) filesToDelete.append(contentsOf: files) } } } // Kill any remaining processes (belt and suspenders approach) for processName in processNamesToKill { try await runPrivilegedCommand("pkill -9 -f '\(processName)' 2>/dev/null || killall -9 '\(processName)' 2>/dev/null || true") } // Collect app paths for artifact in artifacts { if let appArray = artifact["app"] as? [String], let appFullName = appArray.first { let systemAppPath = "/Applications/\(appFullName)" let userAppPath = NSHomeDirectory() + "/Applications/\(appFullName)" if FileManager.default.fileExists(atPath: systemAppPath) { filesToDelete.append(URL(fileURLWithPath: systemAppPath)) } else if FileManager.default.fileExists(atPath: userAppPath) { filesToDelete.append(URL(fileURLWithPath: userAppPath)) } } } } // Collect Caskroom directory let caskroomPath = "\(brewPrefix)/Caskroom/\(token)" if FileManager.default.fileExists(atPath: caskroomPath) { filesToDelete.append(URL(fileURLWithPath: caskroomPath)) } // Delete all collected files in one batch with cask name if !filesToDelete.isEmpty { let bundleName = "\(token) (Homebrew Cask)" let _ = FileManagerUndo.shared.deleteFiles(at: filesToDelete, bundleName: bundleName) } } private func processCaskUninstallDirectives(_ directives: [[String: Any]], caskName: String, allProcessNames: Set, allServiceNames: Set) async throws -> [URL] { // Process directives in the order Homebrew processes them // Based on abstract_uninstall.rb from Homebrew source var filesToDelete: [URL] = [] for directive in directives { if let earlyScript = directive["early_script"] as? [String: Any] { try await handleEarlyScript(earlyScript) } } for directive in directives { if let launchctl = directive["launchctl"] as? String { try await handleLaunchctl(launchctl) } } for directive in directives { if let quit = directive["quit"] as? String { try await handleQuit(quit) } } for directive in directives { if let signal = directive["signal"] as? [Any] { try await handleSignal(signal) } } for directive in directives { if let loginItem = directive["login_item"] as? String { try await handleLoginItem(loginItem) } } for directive in directives { if let kext = directive["kext"] as? String { try await handleKext(kext) } } for directive in directives { if let script = directive["script"] as? [String: Any] { try await handleScript(script) } } for directive in directives { if let pkgutil = directive["pkgutil"] as? String { try await handlePkgutil(pkgutil) } } for directive in directives { if let deleteArray = directive["delete"] as? [String] { for path in deleteArray { let expandedPath = expandPath(path) if FileManager.default.fileExists(atPath: expandedPath) { filesToDelete.append(URL(fileURLWithPath: expandedPath)) } } } else if let deleteString = directive["delete"] as? String { let expandedPath = expandPath(deleteString) if FileManager.default.fileExists(atPath: expandedPath) { filesToDelete.append(URL(fileURLWithPath: expandedPath)) } } } for directive in directives { if let trashArray = directive["trash"] as? [String] { for path in trashArray { let expandedPath = expandPath(path) if FileManager.default.fileExists(atPath: expandedPath) { filesToDelete.append(URL(fileURLWithPath: expandedPath)) } } } else if let trashString = directive["trash"] as? String { let expandedPath = expandPath(trashString) if FileManager.default.fileExists(atPath: expandedPath) { filesToDelete.append(URL(fileURLWithPath: expandedPath)) } } } for directive in directives { if let rmdirArray = directive["rmdir"] as? [String] { for path in rmdirArray { let expandedPath = expandPath(path) if FileManager.default.fileExists(atPath: expandedPath) { filesToDelete.append(URL(fileURLWithPath: expandedPath)) } } } else if let rmdirString = directive["rmdir"] as? String { let expandedPath = expandPath(rmdirString) if FileManager.default.fileExists(atPath: expandedPath) { filesToDelete.append(URL(fileURLWithPath: expandedPath)) } } } return filesToDelete } // MARK: - Formula Uninstall private func uninstallFormula(name: String, info: [String: Any]) async throws { // Try using brew uninstall command first (proper uninstall with symlink cleanup) let arguments = ["uninstall", name, "--force"] do { let result = try await HomebrewController.shared.runBrewCommand(arguments) // Check if brew command failed due to permission error if result.error.contains("Could not remove") && result.error.contains("keg") { // Permission error - fallback to collecting paths for batch deletion var pathsToDelete: [URL] = [] let cellarPath = "\(brewPrefix)/Cellar/\(name)" if FileManager.default.fileExists(atPath: cellarPath) { pathsToDelete.append(URL(fileURLWithPath: cellarPath)) // Also collect symlink paths let optPath = "\(brewPrefix)/opt/\(name)" if FileManager.default.fileExists(atPath: optPath) { pathsToDelete.append(URL(fileURLWithPath: optPath)) } let linkedPath = "\(brewPrefix)/var/homebrew/linked/\(name)" if FileManager.default.fileExists(atPath: linkedPath) { pathsToDelete.append(URL(fileURLWithPath: linkedPath)) } // Batch delete collected paths if !pathsToDelete.isEmpty { let _ = FileManagerUndo.shared.deleteFiles(at: pathsToDelete, bundleName: "Homebrew-\(name)") } } } else if !result.error.isEmpty && !result.error.contains("Warning") { // Other error - throw it throw HomebrewError.commandFailed(result.error) } // Success - brew uninstall handled everything } catch { // Fallback: If brew command itself fails, collect paths for batch deletion var pathsToDelete: [URL] = [] let cellarPath = "\(brewPrefix)/Cellar/\(name)" if FileManager.default.fileExists(atPath: cellarPath) { pathsToDelete.append(URL(fileURLWithPath: cellarPath)) // Collect symlinks let optPath = "\(brewPrefix)/opt/\(name)" if FileManager.default.fileExists(atPath: optPath) { pathsToDelete.append(URL(fileURLWithPath: optPath)) } let linkedPath = "\(brewPrefix)/var/homebrew/linked/\(name)" if FileManager.default.fileExists(atPath: linkedPath) { pathsToDelete.append(URL(fileURLWithPath: linkedPath)) } // Batch delete collected paths if !pathsToDelete.isEmpty { let _ = FileManagerUndo.shared.deleteFiles(at: pathsToDelete, bundleName: "Homebrew-\(name)") } else { throw HomebrewError.commandFailed("Formula \(name) is not installed") } } else { throw HomebrewError.commandFailed("Formula \(name) is not installed") } } } // MARK: - Uninstall Directive Handlers private func handleEarlyScript(_ value: [String: Any]) async throws { guard let executable = value["executable"] as? String else { return } let args = (value["args"] as? [String]) ?? [] let command = ([executable] + args).joined(separator: " ") try await runPrivilegedCommand(command) } private func handleLaunchctl(_ value: String) async throws { // Try both system and user domains let systemPlistPath = "/Library/LaunchDaemons/\(value).plist" let userPlistPath = NSHomeDirectory() + "/Library/LaunchAgents/\(value).plist" // Unload the service and kill the process do { // Try bootout first (modern launchctl) try await runPrivilegedCommand("launchctl bootout system/\(value) 2>/dev/null || launchctl unload \"\(systemPlistPath)\" 2>/dev/null || true") // Force kill the process if still running try await runPrivilegedCommand("pkill -9 -f '\(value)' 2>/dev/null || true") } catch { printOS("Failed to unload system service: \(error.localizedDescription)") } do { try await runPrivilegedCommand("launchctl bootout gui/$UID/\(value) 2>/dev/null || launchctl unload \"\(userPlistPath)\" 2>/dev/null || true") // Force kill the process if still running try await runPrivilegedCommand("pkill -9 -f '\(value)' 2>/dev/null || true") } catch { printOS("Failed to unload user service: \(error.localizedDescription)") } // Delete the plist files using trash var plistPaths: [URL] = [] if FileManager.default.fileExists(atPath: systemPlistPath) { plistPaths.append(URL(fileURLWithPath: systemPlistPath)) } if FileManager.default.fileExists(atPath: userPlistPath) { plistPaths.append(URL(fileURLWithPath: userPlistPath)) } if !plistPaths.isEmpty { let _ = FileManagerUndo.shared.deleteFiles(at: plistPaths, bundleName: "Homebrew-LaunchAgent") } } private func handleQuit(_ value: String) async throws { // First try using the bundle ID with killall (works with bundle IDs) var command = "killall -15 '\(value)' 2>/dev/null || true" do { _ = try await runPrivilegedCommand(command) } catch { printOS("killall failed for \(value): \(error.localizedDescription)") } // Also try pkill with partial match in case it's a process name command = "pkill -15 -f '\(value)' 2>/dev/null || true" do { _ = try await runPrivilegedCommand(command) } catch { printOS("pkill failed for \(value): \(error.localizedDescription)") } } private func handleSignal(_ value: [Any]) async throws { guard value.count >= 2, let signal = value[0] as? String, let process = value[1] as? String else { return } try await runPrivilegedCommand("pkill -\(signal) \(process)") } private func handleLoginItem(_ value: String) async throws { // Unregister using SMAppService let service = SMAppService.loginItem(identifier: value) do { try await service.unregister() } catch { printOS("SMAppService unregister failed for \(value): \(error.localizedDescription)") } } private func handleKext(_ value: String) async throws { try await runPrivilegedCommand("kextunload -b \(value)") } private func handleScript(_ value: [String: Any]) async throws { guard let executable = value["executable"] as? String else { return } let args = (value["args"] as? [String]) ?? [] let command = ([executable] + args).joined(separator: " ") try await runPrivilegedCommand(command) } private func handlePkgutil(_ value: String) async throws { // Get list of files from pkgutil let filesResult = try await runPrivilegedCommand("pkgutil --files \(value) 2>/dev/null || echo ''") let files = filesResult.components(separatedBy: "\n").filter { !$0.isEmpty } // Collect files for batch deletion (they're relative to /) var pathsToDelete: [URL] = [] for file in files { let fullPath = "/\(file)" if FileManager.default.fileExists(atPath: fullPath) { pathsToDelete.append(URL(fileURLWithPath: fullPath)) } } // Batch delete collected paths if !pathsToDelete.isEmpty { let _ = FileManagerUndo.shared.deleteFiles(at: pathsToDelete, bundleName: "Homebrew-PKG-\(value)") } // Forget the package try await runPrivilegedCommand("pkgutil --forget \(value)") } private func handleDelete(_ path: String) async throws { let expandedPath = expandPath(path) if FileManager.default.fileExists(atPath: expandedPath) { // Use trash for all deletions let _ = FileManagerUndo.shared.deleteFiles(at: [URL(fileURLWithPath: expandedPath)], bundleName: "Homebrew-Delete") } } private func handleTrash(_ path: String) async throws { let expandedPath = expandPath(path) if FileManager.default.fileExists(atPath: expandedPath) { // Use FileManagerUndo to properly move to trash let _ = FileManagerUndo.shared.deleteFiles(at: [URL(fileURLWithPath: expandedPath)], bundleName: "Homebrew-Trash") } } private func handleRmdir(_ path: String) async throws { let expandedPath = expandPath(path) if FileManager.default.fileExists(atPath: expandedPath) { // Only remove if empty let contents = try FileManager.default.contentsOfDirectory(atPath: expandedPath) if contents.isEmpty { // Use trash even for empty directories let _ = FileManagerUndo.shared.deleteFiles(at: [URL(fileURLWithPath: expandedPath)], bundleName: "Homebrew-Rmdir") } } } // MARK: - Helper Methods private func expandPath(_ path: String) -> String { if path.hasPrefix("~") { return NSString(string: path).expandingTildeInPath } return path } @discardableResult private func runPrivilegedCommand(_ command: String) async throws -> String { let result = try await runSUCommand( command, errorContext: "Homebrew uninstall operation failed", throwOnFailure: true ) return result.1 } } ================================================ FILE: Pearcleaner/Logic/CLI.swift ================================================ import AlinFoundation import ArgumentParser import Foundation import ServiceManagement import SwiftUI import UniformTypeIdentifiers // Main command structure struct PearCLI: ParsableCommand { static var configuration = CommandConfiguration( commandName: "pear", abstract: "Command-line interface for the Pearcleaner app", subcommands: [ // Run.self, List.self, ListOrphaned.self, Uninstall.self, UninstallAll.self, RemoveOrphaned.self, Helper.self, AskPassword.self, ] ) // For dependency management static var locations: Locations! static var fsm: FolderSettingsManager! // Set up dependencies before running commands static func setupDependencies( locations: Locations, fsm: FolderSettingsManager ) { Self.locations = locations Self.fsm = fsm } // struct Run: ParsableCommand { // static var configuration = CommandConfiguration( // commandName: "run", // abstract: "Launch Pearcleaner in Debug mode to see console logs" // ) // // func run() throws { // printOS("Pearcleaner CLI | Launching App For Debugging:\n") // } // } struct List: ParsableCommand { static var configuration = CommandConfiguration( commandName: "list", abstract: "List application files available for uninstall at the specified path" ) @Argument(help: "Path to the application") var path: String func run() throws { // Convert the provided string path to a URL let url = URL(fileURLWithPath: path) // Fetch the app info and safely unwrap guard let appInfo = AppInfoFetcher.getAppInfo(atPath: url) else { printOS("Error: Invalid path or unable to fetch app info at path: \(path)\n") Foundation.exit(1) } // Use the AppPathFinder to find paths synchronously let appPathFinder = AppPathFinder(appInfo: appInfo, locations: PearCLI.locations) // Call findPaths to get the Set of URLs let foundPaths = appPathFinder.findPathsCLI() // Print each path in the Set to the console for path in foundPaths { printOS(path.path) } printOS("\nFound \(foundPaths.count) application files.\n") Foundation.exit(0) } } struct ListOrphaned: ParsableCommand { static var configuration = CommandConfiguration( commandName: "list-orphaned", abstract: "List orphaned files available for removal" ) func run() throws { // Get installed apps for filtering DispatchQueue.global(qos: .userInitiated).async { let _ = getSortedApps(paths: PearCLI.fsm.folderPaths, useStreaming: false) } // Find orphaned files let foundPaths = ReversePathsSearcher( locations: PearCLI.locations, fsm: PearCLI.fsm, sortedApps: AppState.shared.sortedApps ) .reversePathsSearchCLI() // Print each path in the array to the console for path in foundPaths { printOS(path.path) } printOS("\nFound \(foundPaths.count) orphaned files.\n") Foundation.exit(0) } } struct Uninstall: ParsableCommand { static var configuration = CommandConfiguration( commandName: "uninstall", abstract: "Uninstall only the application bundle at the specified path" ) @Argument(help: "Path to the application") var path: String func run() async throws { // Convert the provided string path to a URL let url = URL(fileURLWithPath: path) // Fetch the app info and safely unwrap guard let appInfo = AppInfoFetcher.getAppInfo(atPath: url) else { printOS("Error: Invalid path or unable to fetch app info at path: \(path)\n") Foundation.exit(1) } // Kill app before deletion await killApp(appId: appInfo.bundleIdentifier) let success = moveFilesToTrashCLI(at: [appInfo.path]) if success { printOS("Application deleted successfully.\n") Foundation.exit(0) } else { printOS("Failed to delete application.\n") Foundation.exit(1) } } } struct UninstallAll: ParsableCommand { static var configuration = CommandConfiguration( commandName: "uninstall-all", abstract: "Uninstall application bundle and ALL related files at the specified path" ) @Argument(help: "Path to the application") var path: String func run() async throws { // Convert the provided string path to a URL let url = URL(fileURLWithPath: path) // Fetch the app info and safely unwrap guard let appInfo = AppInfoFetcher.getAppInfo(atPath: url) else { printOS("Error: Invalid path or unable to fetch app info at path: \(path)") Foundation.exit(1) } // Use the AppPathFinder to find paths synchronously let appPathFinder = AppPathFinder(appInfo: appInfo, locations: PearCLI.locations) // Call findPaths to get the Set of URLs let foundPaths = appPathFinder.findPathsCLI() // Check if any file is protected (non-writable) let protectedFiles = foundPaths.filter { !FileManager.default.isWritableFile(atPath: $0.path) } // If protected files are found, echo message and exit if !protectedFiles.isEmpty && !HelperToolManager.shared.isHelperToolInstalled { printOS("Protected files detected. Please run this command with sudo:\n") printOS("sudo pearcleaner uninstall-all \(path)") printOS("\nProtected files:\n") for file in protectedFiles { printOS(file.path) } Foundation.exit(1) } // Kill app before deletion await killApp(appId: appInfo.bundleIdentifier) let success = moveFilesToTrashCLI(at: Array(foundPaths)) if success { printOS("The application and related files have been deleted successfully.\n") Foundation.exit(0) } else { printOS("Failed to delete some files, they might be protected or in use.\n") Foundation.exit(1) } } } struct RemoveOrphaned: ParsableCommand { static var configuration = CommandConfiguration( commandName: "remove-orphaned", abstract: "Remove ALL orphaned files (To ignore files, add them to the exception list within Pearcleaner settings)" ) func run() throws { // Get installed apps for filtering DispatchQueue.global(qos: .userInitiated).async { let _ = getSortedApps(paths: PearCLI.fsm.folderPaths, useStreaming: false) } // Find orphaned files let foundPaths = ReversePathsSearcher( locations: PearCLI.locations, fsm: PearCLI.fsm, sortedApps: AppState.shared.sortedApps ) .reversePathsSearchCLI() // Check if any file is protected (non-writable) let protectedFiles = foundPaths.filter { !FileManager.default.isWritableFile(atPath: $0.path) } // If protected files are found, echo message and exit if !protectedFiles.isEmpty && !HelperToolManager.shared.isHelperToolInstalled { printOS("Protected files detected. Please run this command with sudo:\n") printOS("sudo pearcleaner remove-orphaned") printOS("\nProtected files:\n") for file in protectedFiles { printOS(file.path) } Foundation.exit(1) } let success = moveFilesToTrashCLI(at: foundPaths) if success { printOS("Orphaned files have been deleted successfully.\n") Foundation.exit(0) } else { printOS("Failed to delete some orphaned files.\n") Foundation.exit(1) } } } struct Helper: ParsableCommand { static var configuration = CommandConfiguration( commandName: "helper", abstract: "Manage privileged helper tool status" ) @Argument(help: "Action: 'enable' or 'disable'. Omit to check status.") var action: String? func run() throws { // If no action provided, return status guard let action = action else { let semaphore = DispatchSemaphore(value: 0) var isEnabled = false Task { isEnabled = await isHelperEnabled() semaphore.signal() } semaphore.wait() let status = isEnabled ? "Enabled" : "Disabled" printOS(status) Foundation.exit(0) } // Validate action guard ["enable", "disable"].contains(action.lowercased()) else { printOS("Error: Invalid action. Use 'enable', 'disable', or omit for status.\n") Foundation.exit(1) } // Check current status first let semaphore1 = DispatchSemaphore(value: 0) var currentlyEnabled = false Task { currentlyEnabled = await isHelperEnabled() semaphore1.signal() } semaphore1.wait() // Pre-check before attempting operation if action.lowercased() == "enable" { if currentlyEnabled { printOS("Privileged helper is already enabled.\n") Foundation.exit(0) } } else { if !currentlyEnabled { printOS("Privileged helper is already disabled.\n") Foundation.exit(0) } } // Proceed with enable/disable operation let semaphore2 = DispatchSemaphore(value: 0) var operationSuccess = false var errorMessage: String? Task { if action.lowercased() == "enable" { await HelperToolManager.shared.manageHelperTool(action: .install) operationSuccess = await isHelperEnabled() if !operationSuccess { errorMessage = "Failed to enable privileged helper" } } else { await HelperToolManager.shared.manageHelperTool(action: .uninstall) operationSuccess = !(await isHelperEnabled()) if !operationSuccess { errorMessage = "Failed to disable privileged helper" } } semaphore2.signal() } // Wait for async operation to complete semaphore2.wait() if operationSuccess { if action.lowercased() == "enable" { printOS("Privileged helper enabled successfully.\n") } else { printOS("Privileged helper disabled successfully.\n") } Foundation.exit(0) } else { printOS("Error: \(errorMessage ?? "Unknown error occurred")\n") Foundation.exit(1) } } // Helper function to check if privileged helper is enabled private func isHelperEnabled() async -> Bool { let result = try! await runSUCommand("whoami", skipHelperCheck: true) return result.0 && result.1.trimmingCharacters(in: .whitespacesAndNewlines) == "root" } } struct AskPassword: ParsableCommand { static var configuration = CommandConfiguration( commandName: "ask-password", abstract: "Display password prompt for sudo operations", shouldDisplay: false ) @Option(name: .long, help: .hidden) var message: String = "Homebrew is requesting your password to perform a privileged action" func run() throws { // Check keychain first if let cached = KeychainPasswordManager.shared.retrievePassword() { print(cached) Darwin.exit(0) } // Not cached, get fresh password guard let password = obtainPassword() else { Darwin.exit(1) } // Save to keychain (uses user-configured timeout from settings) KeychainPasswordManager.shared.savePassword(password) // Print and exit immediately print(password) Darwin.exit(0) } // MARK: - Obtain Password private func obtainPassword() -> String? { // Check if Pearcleaner main app is running let runningApps = NSWorkspace.shared.runningApplications let pearcleanerRunning = runningApps.contains { app in app.bundleIdentifier == "com.alienator88.Pearcleaner" && app.processIdentifier != ProcessInfo.processInfo.processIdentifier } if pearcleanerRunning { return requestPasswordFromMainApp() } else { _ = NSApplication.shared return Self.showPasswordDialog(message: message) } } // MARK: - Request Password from Main App private func requestPasswordFromMainApp() -> String? { let center = DistributedNotificationCenter.default() let requestId = UUID().uuidString var receivedPassword: String? let semaphore = DispatchSemaphore(value: 0) let observerQueue = OperationQueue() let observer = center.addObserver( forName: NSNotification.Name("com.alienator88.Pearcleaner.passwordResponse"), object: nil, queue: observerQueue ) { notification in if let userInfo = notification.userInfo, let responseId = userInfo["requestId"] as? String, responseId == requestId { receivedPassword = userInfo["password"] as? String semaphore.signal() } } center.postNotificationName( NSNotification.Name("com.alienator88.Pearcleaner.passwordRequest"), object: nil, userInfo: [ "requestId": requestId, "message": message ], deliverImmediately: true ) let timeout = DispatchTime.now() + .seconds(60) if semaphore.wait(timeout: timeout) == .success { center.removeObserver(observer) return receivedPassword?.isEmpty == false ? receivedPassword : nil } else { center.removeObserver(observer) return nil } } // MARK: - Show Password Dialog private static func showPasswordDialog(message: String) -> String? { let alert = NSAlert() alert.messageText = "Pearcleaner" alert.informativeText = message alert.alertStyle = .informational alert.addButton(withTitle: "OK") alert.addButton(withTitle: "Cancel") let secureTextField = NSSecureTextField(frame: NSRect(x: 0, y: 0, width: 300, height: 24)) secureTextField.placeholderString = "Password" alert.accessoryView = secureTextField alert.window.initialFirstResponder = secureTextField NSApp.activate(ignoringOtherApps: true) let response = alert.runModal() if response == .alertFirstButtonReturn { let password = secureTextField.stringValue return password.isEmpty ? nil : password } return nil } } } ================================================ FILE: Pearcleaner/Logic/Conditions.swift ================================================ // // Conditions.swift // Pearcleaner // // Created by Alin Lupascu on 4/15/24. // import Foundation struct Condition: Codable { var bundle_id: String var include: [String] var exclude: [String] var includeForce: [URL]? var excludeForce: [URL]? init(bundle_id: String, include: [String], exclude: [String], includeForce: [String]? = nil, excludeForce: [String]? = nil) { self.bundle_id = bundle_id.pearFormat() self.include = include.map { $0.pearFormat() } self.exclude = exclude.map { $0.pearFormat() } self.includeForce = includeForce?.compactMap { path in if let url = URL(string: path), FileManager.default.fileExists(atPath: url.path) { return url } return nil } self.excludeForce = excludeForce?.compactMap { path in if let url = URL(string: path), FileManager.default.fileExists(atPath: url.path) { return url } return nil } } } struct SkipCondition { var skipPrefix: [String] var allowPrefixes: [String] var skipPaths: [String] } // Conditions for some apps that need to include/exclude certain files/folders when names are more complicated var conditions: [Condition] = [ Condition( bundle_id: "com.apple.dt.xcode", include: ["com.apple.dt", "xcode", "simulator"], exclude: ["com.robotsandpencils.xcodesapp", "com.xcodesorg.xcodesapp", "com.oneminutegames.xcodecleaner", "io.hyperapp.xcodecleaner", "available-xcodes", "xcodes", "cleaner for xcode"], includeForce: ["\(home)/Library/Containers/com.apple.iphonesimulator.ShareExtension"] ), Condition( bundle_id: "com.robotsandpencils.xcodesapp", include: [], exclude: ["com.apple.dt.xcode", "com.oneminutegames.xcodecleaner", "io.hyperapp.xcodecleaner"], includeForce: nil ), Condition( bundle_id: "com.xcodesorg.xcodesapp", include: [], exclude: ["com.apple.dt.xcode", "com.oneminutegames.xcodecleaner", "io.hyperapp.xcodecleaner"], includeForce: nil ), Condition( bundle_id: "io.hyperapp.xcodecleaner", include: [], exclude: ["com.robotsandpencils.xcodesapp", "com.oneminutegames.xcodecleaner", "com.apple.dt.xcode", "xcodes.json"], includeForce: nil ), Condition( bundle_id: "us.zoom.xos", include: ["zoom"], exclude: [], includeForce: nil ), Condition( bundle_id: "com.brave.browser", include: ["brave"], exclude: [], includeForce: nil ), Condition( bundle_id: "com.okta.mobile", include: ["okta"], exclude: [], includeForce: nil ), Condition( bundle_id: "com.google.chrome", include: ["google", "chrome"], exclude: ["iterm", "chromefeaturestate", "monochrome"], includeForce: nil ), Condition( bundle_id: "com.microsoft.edgemac", include: [], exclude: ["vscode", "rdc", "appcenter", "office", "oneauth"], includeForce: nil ), Condition( bundle_id: "com.microsoft.teams2", include: [], exclude: ["office"], includeForce: nil ), Condition( bundle_id: "org.mozilla.firefox", include: ["firefox"], exclude: ["thunderbird"], includeForce: nil ), Condition( bundle_id: "org.mozilla.thunderbird", include: [], exclude: ["firefox"], includeForce: nil ), Condition( bundle_id: "org.mozilla.firefox.nightly", include: ["mozilla", "firefox"], exclude: ["thunderbird"], includeForce: nil ), Condition( bundle_id: "com.logi.optionsplus", include: ["logi", "logipluginservice"], exclude: ["login", "logic"], includeForce: nil ), Condition( bundle_id: "com.microsoft.VSCode", include: ["vscode"], exclude: ["vscodeinsiders", "insiders"], includeForce: ["\(home)/Library/Application Support/Code/"] ), Condition( bundle_id: "com.microsoft.VSCodeInsiders", include: ["vscodeinsiders", "insiders"], exclude: [], includeForce: ["\(home)/Library/Application Support/Code - Insiders/"] ), Condition( bundle_id: "com.facebook.archon.developerid", include: ["archon.loginhelper"], exclude: [], includeForce: nil ), Condition( bundle_id: "eu.exelban.stats", include: [], exclude: ["video"], includeForce: nil ), Condition( bundle_id: "me.mhaeuser.BatteryToolkit", include: ["memhaeuser"], exclude: [], includeForce: nil ), Condition( bundle_id: "jetbrains", include: ["jcef"], exclude: [], includeForce: ["\(home)/Library/Application Support/JetBrains/", "\(home)/Library/Caches/JetBrains/", "\(home)/Library/Logs/JetBrains/"] ), Condition( bundle_id: "company.thebrowser.Browser", include: ["firestore"], exclude: [], includeForce: ["\(home)/Library/Application Support/Arc/", "\(home)/Library/Caches/Arc/"] ), Condition( bundle_id: "com.1password.1password", include: ["waveboxapp", "sidekick"], exclude: [], includeForce: nil ), Condition( bundle_id: "com.now.gg.BlueStacks", include: ["bst_boost_interprocess"], exclude: [], includeForce: nil ), Condition( bundle_id: "com.electron.sdm", include: ["strongdm"], exclude: [], includeForce: nil ), Condition( bundle_id: "com.github.githubclient", include: ["comgithubelectron"], exclude: [], includeForce: nil ), Condition( bundle_id: "com.native-instruments.nativeaccess", include: ["comnative", "nativeinstruments"], exclude: [], includeForce: nil ), ] // Skip some system files/folders let skipConditions: [SkipCondition] = [ SkipCondition( skipPrefix: ["mobiledocuments", "reminders", "dsstore", "comapplepasswordmanager"], allowPrefixes: ["comappleconfigurator", "comappledt", "comappleiwork", "comapplesfsymbols", "comappletestflight", "comapplesharedfilelist", "comapplelssharedfilelist"], skipPaths: ["\(home)/.Trash", "/Library/SystemExtensions", "/System/Volumes/Preboot/Cryptexes/App/System/Library/CoreServices/PasswordManagerBrowserExtensionHelper.app/Contents/MacOS/PasswordManagerBrowserExtensionHelper", "\(home)/Library/Application Support/Chromium/NativeMessagingHosts/com.apple.passwordmanager.json", "\(home)/Library/Application Support/Google/Chrome/NativeMessagingHosts/com.apple.passwordmanager.json"] ) ] // Library subdirectories that should be excluded from deep (depth=2) search // These are macOS system directories that never contain third-party app files let skipDeepSearch: Set = [ // Core System "Apple", "Audio", "Bluetooth", "ColorSync", "Components", "CoreAnalytics", "CoreMediaIO", "DirectoryServices", "Filesystems", "GPUBundles", "Graphics", "KernelCollections", "OSAnalytics", "OpenDirectory", "Sandbox", "Security", "SystemExtensions", "SystemMigration", "SystemProfiler", "StagedDriverExtensions", "StagedExtensions", "StartupItems", // User Data & System Services (should not be searched) "Accessibility", "Accounts", "AppleMediaServices", "Assistant", "Assistants", "Autosave Information", "Biome", "Calendars", "CallServices", "CloudStorage", "Contacts", "Cookies", "DataAccess", "DataDeliveryServices", "DoNotDisturb", "DuetExpertCenter", "Finance", "FinanceBackup", "FrontBoard", "GameKit", "GroupContainersAlias", "HomeKit", "IdentityServices", "IntelligencePlatform", "Intents", "KeyboardServices", "LanguageModeling", "LockdownMode", "Mail", "MediaAnalysis", "Messages", "Metadata", "Mobile Documents", "MobileDevice", "News", "Passes", "PersonalizationPortrait", "Photos", "PrivateCloudCompute", "Reminders", "ResponseKit", "Safari", "SafariSafeBrowsing", "SafariSandboxBroker", "ScreenRecordings", "StatusKit", "Suggestions", "SyncedPreferences", "Translation", "UnifiedAssetFramework", "Weather", "homeenergyd", "studentd", // Development/System Tools "Developer", "Perl", "Ruby", "Java", "Python", "Catacomb", "InstallerSandboxes", "Trial", "Updates", "Staging", "ContainerManager", "Daemon Containers", // Additional System Directories "ColorPickers", "Colors", "Compositions", "Contextual Menu Items", "Documentation", "DriverExtensions", "Favorites", "FontCollections", "Fonts", "Image Capture", "Input Methods", "Jupyter", "Keyboard", "Keyboard Layouts", "Keychains", "Managed Preferences", "PDF Services", "Printers", "QuickLook", "Receipts", "Screen Savers", "ScriptingAdditions", "Scripts", "Sharing", "Shortcuts", "Sounds", "Speech", "Spelling", "Spotlight", "User Pictures", "User Template", "Video", "WebServer", "Workflows", // Apple service bundles (com.apple.*) "com.apple.AppleMediaServices", "com.apple.WatchListKit", "com.apple.aiml.instrumentation", "com.apple.appleaccountd", "com.apple.bluetooth.services.cloud", "com.apple.bluetoothuser", "com.apple.familycircled", "com.apple.iTunesCloud", "com.apple.internal.ck" ] // Skip files/folders during orphaned file search let skipReverse = ["apple", "temporary", "btserver", "proapps", "scripteditor", "ilife", "livefsd", "siritoday", "addressbook", "animoji", "appstore", "askpermission", "callhistory", "clouddocs", "diskimages", "dock", "facetime", "fileprovider", "instruments", "knowledge", "mobilesync", "syncservices", "homeenergyd", "icloud", "icdd", "networkserviceproxy", "familycircle", "geoservices", "installation", "passkit", "sharedimagecache", "desktop", "mbuseragent", "swiftpm", "baseband", "coresimulator", "photoslegacyupgrade", "photosupgrade", "siritts", "ipod", "globalpreferences", "apmanalytics", "apmexperiment", "avatarcache", "byhost", "contextstoreagent", "mobilemeaccounts", "mobiledocuments", "mobile", "intentbuilderc", "loginwindow", "momc", "replayd", "sharedfilelistd", "clang", "audiocomponent", "csexattrcryptoservice", "livetranscriptionagent", "sandboxhelper", "statuskitagent", "betaenrollmentd", "contentlinkingd", "diagnosticextensionsd", "gamed", "heard", "homed", "itunescloudd", "lldb", "mds", "mediaanalysisd", "metrickitd", "mobiletimerd", "proactived", "ptpcamerad", "studentd", "talagent", "watchlistd", "apptranslocation", "xcrun", "ds_store", "caches", "crashreporter", "trash", "pearcleaner", "amsdatamigratortool", "arfilecache", "assistant", "chromium", "cloudkit", "webkit", "databases", "diagnostic", "cache", "gamekit", "homebrew", "logi", "microsoft", "mozilla", "sync", "google", "sentinel", "hexnode", "sentry", "tvappservices", "reminders", "pbs", "notarytool", "differentialprivacy", "storeassetd", "webpush", "storedownloadd", "fsck", "crash", "python", "discrecording", "photossearch", "pylint", "jamf", "scopedbookmarkagent", "anonymous", "identifier", "isolated", "nobackup", "privacypreservingmeasurement", "symbols", "stickersd", "privatecloudcomputed", "tipsd", "controlcenter", "contactsd", "staticcheck", "index", "segment", "sparkle", "summaryevents", "launchdarkly", "identityservicesd", "embeddedbinaryvalidationutility", "comalienator88", "aaprofilepicture", "minilauncher", "jna", "automator", "locationaccessstored", "spotlight", "cef"] ================================================ FILE: Pearcleaner/Logic/DeepLink.swift ================================================ // // DeepLink.swift // Pearcleaner // // Created by Alin Lupascu on 11/9/23. // import Foundation import SwiftUI import AlinFoundation class DeeplinkManager { private var urlQueue: [URL] = [] private var isProcessing = false let updater: Updater let fsm: FolderSettingsManager @AppStorage("settings.general.oneshot") private var oneShotMode: Bool = false @State private var windowController = WindowManager() init(updater: Updater, fsm: FolderSettingsManager) { self.updater = updater self.fsm = fsm if AppState.shared.currentPage != .applications { updateOnMain { AppState.shared.currentPage = .applications } } } struct DeepLinkActions { static let openPearcleaner = "openPearcleaner" static let openSettings = "openSettings" static let openPermissions = "openPermissions" static let uninstallApp = "uninstallApp" static let checkOrphanedFiles = "checkOrphanedFiles" static let checkDevEnv = "checkDevEnv" static let appLipo = "appLipo" static let checkUpdates = "checkUpdates" static let appsPaths = "appsPaths" static let orphanedPaths = "orphanedPaths" static let refreshAppsList = "refreshAppsList" static let resetSettings = "resetSettings" static let allActions = [ openPearcleaner, openSettings, openPermissions, uninstallApp, checkOrphanedFiles, checkDevEnv, appLipo, checkUpdates, appsPaths, orphanedPaths, refreshAppsList, resetSettings ] } func manage(url: URL, appState: AppState, locations: Locations) { // Set externalMode to true updateOnMain { appState.externalMode = true } guard let scheme = url.scheme, scheme == "pear" else { guard !url.path.isEmpty else { printOS("DLM: URL path is empty.") return } handleAsPathOrDropped(url: url, appState: appState, locations: locations) return } if let host = url.host, DeepLinkActions.allActions.contains(host) { switch host { case DeepLinkActions.uninstallApp: handleAsPathOrDropped(url: url, appState: appState, locations: locations) default: if let components = URLComponents(url: url, resolvingAgainstBaseURL: true), let queryItems = components.queryItems { handleAppFunctions(action: host, queryItems: queryItems, appState: appState, fsm: fsm) } else { handleAppFunctions(action: host, queryItems: [], appState: appState, fsm: fsm) } } } else { // Host is nil or not in actions, treat as dropped/path scenario handleAsPathOrDropped(url: url, appState: appState, locations: locations) } } private func handleAsPathOrDropped(url: URL, appState: AppState, locations: Locations) { urlQueue.append(url) processQueue(appState: appState, locations: locations) if appState.appInfo.isEmpty { loadNextAppInfo(appState: appState, locations: locations) } } private func processQueue(appState: AppState, locations: Locations) { guard !isProcessing, let nextURL = urlQueue.first else { return } isProcessing = true // Process the next URL in the queue if nextURL.pathExtension == "app" { handleDroppedApps(url: nextURL, appState: appState, locations: locations) } else if nextURL.scheme == "pear" { handleDeepLinkedApps(url: nextURL, appState: appState, locations: locations) } // Remove processed URL and set up for the next one urlQueue.removeFirst() isProcessing = false // Process the next URL if there are any left in the queue if !urlQueue.isEmpty { processQueue(appState: appState, locations: locations) } } private func handleDroppedApps(url: URL, appState: AppState, locations: Locations) { // Ensure the dropped app path is added only if it's not already in externalPaths if !appState.externalPaths.contains(url) { appState.externalPaths.append(url) } // If no app is currently loaded, load the first app in the array if appState.appInfo.isEmpty { loadNextAppInfo(appState: appState, locations: locations) } } func handleDeepLinkedApps(url: URL, appState: AppState, locations: Locations) { if let components = URLComponents(url: url, resolvingAgainstBaseURL: true), let queryItems = components.queryItems { // Check for "path" query item first if let path = queryItems.first(where: { $0.name == "path" })?.value { let pathURL = URL(fileURLWithPath: path) guard FileManager.default.fileExists(atPath: pathURL.path) else { printOS("DLM: sent path doesn't exist: \(pathURL.path)") return } // Add path only if it's not already in externalPaths if !appState.externalPaths.contains(pathURL) { appState.externalPaths.append(pathURL) } // Load the first app in externalPaths if no app is currently loaded if appState.appInfo.isEmpty { loadNextAppInfo(appState: appState, locations: locations) } } // If "path" is not available, check for "name" query item else if let name = queryItems.first(where: { $0.name == "name" })?.value?.lowercased() { reloadAppsList(appState: appState, fsm: fsm) { let matchType = queryItems.first(where: { $0.name == "matchType" })?.value?.lowercased() ?? "exact" if let matchedApp = appState.sortedApps.first(where: { appInfo in let appNameLowercased = appInfo.appName.lowercased() switch matchType { case "contains": return appNameLowercased.contains(name) case "exact": return appNameLowercased == name default: return false } }) { let pathURL = matchedApp.path // Add path only if it's not already in externalPaths if !appState.externalPaths.contains(pathURL) { appState.externalPaths.append(pathURL) } // Load the first app in externalPaths if no app is currently loaded if appState.appInfo.isEmpty { self.loadNextAppInfo(appState: appState, locations: locations) } } else { printOS("DLM: No app found matching the name '\(name)' with matchType: \(matchType)") } } } else { printOS("DLM: No valid query items for 'path' or 'name' found in the URL.") } } else { printOS("DLM: URL does not match the expected scheme pear://") } } private func loadNextAppInfo(appState: AppState, locations: Locations) { guard let nextPath = appState.externalPaths.first else { return } // Fetch app info guard let appInfo = AppInfoFetcher.getAppInfo(atPath: nextPath) else { printOS("DLM: Failed to get appInfo for path: \(nextPath.path)") return } // Pass the appInfo and trigger showAppInFiles to handle display and animations showAppInFiles(appInfo: appInfo, appState: appState, locations: locations) } private func handleAppFunctions(action: String, queryItems: [URLQueryItem], appState: AppState, fsm: FolderSettingsManager) { switch action { case DeepLinkActions.openPearcleaner: appState.currentPage = .applications break case DeepLinkActions.openSettings: if let page = queryItems.first(where: { $0.name == "name" })?.value { // Query parameter provided - match to specific tab let search = page.lowercased() let allPages = CurrentTabView.allCases if let matchedPage = allPages.first(where: { $0.title.lowercased().contains(search) }) { openAppSettingsWindow(tab: matchedPage, updater: updater) } } else { // No query parameter - open with saved preference (or general if none saved) openAppSettingsWindow(updater: updater) } break case DeepLinkActions.openPermissions: windowController.open(with: PermissionsSheetView().ignoresSafeArea(), width: 300, height: 250, material: .hudWindow) break case DeepLinkActions.checkOrphanedFiles: appState.currentPage = .orphans break case DeepLinkActions.checkDevEnv: if let envName = queryItems.first(where: { $0.name == "name" })?.value { let search = envName.lowercased() let allEnvs = PathLibrary.getPaths() if let matchedEnv = allEnvs.first(where: { $0.name.lowercased().contains(search) }) { updateOnMain() { appState.selectedEnvironment = matchedEnv } } } appState.currentPage = .development break case DeepLinkActions.appLipo: appState.currentPage = .lipo break case DeepLinkActions.checkUpdates: updater.checkForUpdates(sheet: true) break case DeepLinkActions.appsPaths: if let actionType = queryItems.first(where: { $0.name == "add" || $0.name == "remove" })?.name, let pathValue = queryItems.first(where: { $0.name == "path" })?.value { var isDirectory: ObjCBool = false if FileManager.default.fileExists(atPath: pathValue, isDirectory: &isDirectory), isDirectory.boolValue { switch actionType { case "add": fsm.addPath(pathValue) case "remove": fsm.removePath(pathValue) default: printOS("DLM: Invalid action type for appsPaths: \(actionType)") } } else { printOS("DLM: Provided path '\(pathValue)' does not exist or is not a directory.") } } else { printOS("DLM: Missing 'add' or 'remove' action, or 'path' query item for appsPaths.") } break case DeepLinkActions.orphanedPaths: if let actionType = queryItems.first(where: { $0.name == "add" || $0.name == "remove" })?.name, let pathValue = queryItems.first(where: { $0.name == "path" })?.value { switch actionType { case "add": fsm.addPathZ(pathValue) case "remove": fsm.removePathZ(pathValue) default: printOS("DLM: Invalid action type for orphanedPaths: \(actionType)") } } else { printOS("DLM: Missing 'add' or 'remove' action, or 'path' query item for orphanedPaths.") } break case DeepLinkActions.refreshAppsList: reloadAppsList(appState: appState, fsm: fsm) break case DeepLinkActions.resetSettings: DispatchQueue.global(qos: .background).async { UserDefaults.standard.dictionaryRepresentation().keys.forEach(UserDefaults.standard.removeObject(forKey:)) } break default: break } } } ================================================ FILE: Pearcleaner/Logic/FileSearch/FileSearchLogic.swift ================================================ // // FileSearchLogic.swift // Pearcleaner // // Created by Alin Lupascu on 09/29/25. // import Foundation import AppKit import AlinFoundation // Import SearchType from FileSearchView enum SearchType: String, CaseIterable { case filesAndFolders = "Files & Folders" case filesOnly = "Files Only" case foldersOnly = "Folders Only" } class FileSearchEngine { private var shouldStop = false private let fileManager = FileManager.default private var caseSensitive = false private var searchType: SearchType = .filesAndFolders private var excludeSystemFolders = true private var searchRootPath = "" // Common system folder names to exclude (relative to macOS root) private let systemFoldersToExclude = [ "System", "private", "usr", "bin", "sbin", "cores", "dev", "etc" ] func stop() { shouldStop = true } func search( rootPath: String, filters: [FilterType], includeSubfolders: Bool, includeHiddenFiles: Bool, caseSensitive: Bool, searchType: SearchType, excludeSystemFolders: Bool, onBatchFound: @escaping ([FileSearchResult]) -> Void, completion: @escaping () -> Void ) { self.caseSensitive = caseSensitive self.searchType = searchType self.excludeSystemFolders = excludeSystemFolders self.searchRootPath = rootPath Task(priority: .high) { await performSearch( rootPath: rootPath, filters: filters, includeSubfolders: includeSubfolders, includeHiddenFiles: includeHiddenFiles, onBatchFound: onBatchFound ) completion() } } // Overloaded version that accepts multiple root paths func search( rootPaths: [String], filters: [FilterType], includeSubfolders: Bool, includeHiddenFiles: Bool, caseSensitive: Bool, searchType: SearchType, excludeSystemFolders: Bool, onBatchFound: @escaping ([FileSearchResult]) -> Void, completion: @escaping () -> Void ) { self.caseSensitive = caseSensitive self.searchType = searchType self.excludeSystemFolders = excludeSystemFolders Task(priority: .high) { for rootPath in rootPaths { if shouldStop { break } self.searchRootPath = rootPath await performSearch( rootPath: rootPath, filters: filters, includeSubfolders: includeSubfolders, includeHiddenFiles: includeHiddenFiles, onBatchFound: onBatchFound ) } completion() } } private func performSearch( rootPath: String, filters: [FilterType], includeSubfolders: Bool, includeHiddenFiles: Bool, onBatchFound: @escaping ([FileSearchResult]) -> Void ) async { var batch: [FileSearchResult] = [] let batchSize = 1 // Update UI as soon as first result is found for immediate feedback // Pre-specify resource keys for better performance let resourceKeys: [URLResourceKey] = [ .totalFileSizeKey, .contentModificationDateKey, .creationDateKey, .isAliasFileKey, .isPackageKey, .tagNamesKey, .fileResourceIdentifierKey // For Finder comments via extended attributes ] if includeSubfolders { // Check if this path looks like a macOS root (has typical system structure) if isMacOSRootStructure(rootPath) { // Prioritize Users folder first for faster results let usersPath = (rootPath as NSString).appendingPathComponent("Users") await searchFolder(usersPath, resourceKeys: resourceKeys, includeHiddenFiles: includeHiddenFiles, filters: filters, batch: &batch, batchSize: batchSize, onBatchFound: onBatchFound) if shouldStop { return } // Then search other top-level directories do { let topLevelContents = try fileManager.contentsOfDirectory(at: URL(fileURLWithPath: rootPath), includingPropertiesForKeys: [], options: []) for topLevelURL in topLevelContents { if shouldStop { break } // Skip Users since we already searched it if topLevelURL.lastPathComponent == "Users" { continue } // Skip system folders if enabled if excludeSystemFolders && shouldExcludeSystemPath(topLevelURL.path) { continue } await searchFolder(topLevelURL.path, resourceKeys: resourceKeys, includeHiddenFiles: includeHiddenFiles, filters: filters, batch: &batch, batchSize: batchSize, onBatchFound: onBatchFound) } } catch { // Silently skip directories that don't exist or can't be read } } else { // For non-macOS-root paths, use standard enumerator await searchFolder(rootPath, resourceKeys: resourceKeys, includeHiddenFiles: includeHiddenFiles, filters: filters, batch: &batch, batchSize: batchSize, onBatchFound: onBatchFound) } } else { // Single-level search do { let contents = try fileManager.contentsOfDirectory( at: URL(fileURLWithPath: rootPath), includingPropertiesForKeys: resourceKeys, options: includeHiddenFiles ? [] : [.skipsHiddenFiles] ) for fileURL in contents { if shouldStop { break } // Skip system folders if enabled if excludeSystemFolders && shouldExcludeSystemPath(fileURL.path) { continue } if let result = await processItem(url: fileURL, filters: filters) { batch.append(result) if batch.count >= batchSize { await flushBatch(&batch, onBatchFound: onBatchFound) } } } } catch { // Silently skip directories that don't exist or can't be read } } // Flush remaining items if !batch.isEmpty { await flushBatch(&batch, onBatchFound: onBatchFound) } } private func searchFolder( _ folderPath: String, resourceKeys: [URLResourceKey], includeHiddenFiles: Bool, filters: [FilterType], batch: inout [FileSearchResult], batchSize: Int, onBatchFound: @escaping ([FileSearchResult]) -> Void ) async { guard let enumerator = fileManager.enumerator( at: URL(fileURLWithPath: folderPath), includingPropertiesForKeys: resourceKeys, options: includeHiddenFiles ? [] : [.skipsHiddenFiles] ) else { return } // Drive the enumerator manually to avoid Sequence.makeIterator in async context (Swift 6) while !shouldStop, let fileURL = enumerator.nextObject() as? URL { // Skip system folders if enabled if excludeSystemFolders && shouldExcludeSystemPath(fileURL.path) { continue } // Check if item matches all filters if let result = await processItem(url: fileURL, filters: filters) { batch.append(result) // Flush batch when it reaches batch size if batch.count >= batchSize { await flushBatch(&batch, onBatchFound: onBatchFound) } } } } private func flushBatch( _ batch: inout [FileSearchResult], onBatchFound: @escaping ([FileSearchResult]) -> Void ) async { let batchCopy = batch batch.removeAll() await MainActor.run { onBatchFound(batchCopy) } } private func processItem(url: URL, filters: [FilterType]) async -> FileSearchResult? { // Use hasDirectoryPath for faster directory detection let isDirectory = url.hasDirectoryPath let name = url.lastPathComponent let type = isDirectory ? "Folder" : (url.pathExtension.isEmpty ? "File" : url.pathExtension.uppercased()) // Apply search type filter first (early return for performance) switch searchType { case .filesOnly: if isDirectory { return nil } case .foldersOnly: if !isDirectory { return nil } case .filesAndFolders: break } do { let resourceValues = try url.resourceValues( forKeys: [.totalFileSizeKey, .contentModificationDateKey, .creationDateKey, .isAliasFileKey, .isPackageKey, .tagNamesKey] ) let isAlias = resourceValues.isAliasFile ?? false let isPackage = resourceValues.isPackage ?? false // Only get size for files, not directories (optimization) let size = isDirectory ? 0 : (resourceValues.totalFileSize.map { Int64($0) } ?? 0) let dateModified = resourceValues.contentModificationDate ?? Date() let dateCreated = resourceValues.creationDate ?? Date() let tags = resourceValues.tagNames ?? [] // Get Finder comment via extended attributes let comment = getFinderComment(for: url) // Apply all filters for filter in filters { if !matchesFilter( filter: filter, name: name, url: url, type: type, size: size, dateModified: dateModified, dateCreated: dateCreated, isDirectory: isDirectory, isAlias: isAlias, isPackage: isPackage, tags: tags, comment: comment ) { return nil // Does not match this filter, skip } } // Get icon let icon = getIconForFileOrFolderNS(atPath: url) return FileSearchResult( url: url, name: name, type: type, size: size, dateModified: dateModified, isDirectory: isDirectory, icon: icon ) } catch { return nil } } private func matchesFilter( filter: FilterType, name: String, url: URL, type: String, size: Int64, dateModified: Date, dateCreated: Date, isDirectory: Bool, isAlias: Bool, isPackage: Bool, tags: [String], comment: String ) -> Bool { switch filter { case .name(let nameFilter, let value): return matchesNameFilter(nameFilter: nameFilter, name: name, value: value) case .fileExtension(let extFilter, let extensions): let extList = extensions.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces).lowercased() } let fileExt = url.pathExtension.lowercased() switch extFilter { case .includes: return extList.contains(fileExt) case .excludes: return !extList.contains(fileExt) } case .size(let sizeFilter, let value, let max): return matchesSizeFilter(sizeFilter: sizeFilter, size: size, value: value, max: max) case .date(let dateFilter, let value, let end): return matchesDateFilter(dateFilter: dateFilter, dateModified: dateModified, dateCreated: dateCreated, value: value, end: end) case .kind(let kindFilter): return matchesKindFilter(kindFilter: kindFilter, isDirectory: isDirectory, isAlias: isAlias, isPackage: isPackage) case .tags(let tagFilter, let value): return matchesTagsFilter(tagFilter: tagFilter, tags: tags, value: value) case .comment(let commentFilter, let value): return matchesCommentFilter(commentFilter: commentFilter, comment: comment, value: value) } } private func matchesNameFilter(nameFilter: NameFilterType, name: String, value: String) -> Bool { let nameToCompare = caseSensitive ? name : name.lowercased() let valueToCompare = caseSensitive ? value : value.lowercased() switch nameFilter { case .contains: return nameToCompare.contains(valueToCompare) case .doesntContain: return !nameToCompare.contains(valueToCompare) case .startsWith: return nameToCompare.hasPrefix(valueToCompare) case .endsWith: return nameToCompare.hasSuffix(valueToCompare) case .equals: return nameToCompare == valueToCompare case .regex: let regexOptions: NSRegularExpression.Options = caseSensitive ? [] : [.caseInsensitive] guard let regex = try? NSRegularExpression(pattern: value, options: regexOptions) else { return false } let range = NSRange(name.startIndex..., in: name) return regex.firstMatch(in: name, range: range) != nil } } private func matchesSizeFilter(sizeFilter: SizeFilterType, size: Int64, value: Int64, max: Int64?) -> Bool { switch sizeFilter { case .greaterThan: return size > value case .lessThan: return size < value case .between: guard let max = max else { return false } return size >= value && size <= max case .equals: return size == value } } private func matchesDateFilter( dateFilter: DateFilterType, dateModified: Date, dateCreated: Date, value: Date, end: Date? ) -> Bool { switch dateFilter { case .createdBefore: return dateCreated < value case .createdAfter: return dateCreated > value case .createdBetween: guard let end = end else { return false } return dateCreated >= value && dateCreated <= end case .modifiedBefore: return dateModified < value case .modifiedAfter: return dateModified > value case .modifiedBetween: guard let end = end else { return false } return dateModified >= value && dateModified <= end } } private func matchesKindFilter( kindFilter: KindFilterType, isDirectory: Bool, isAlias: Bool, isPackage: Bool ) -> Bool { switch kindFilter { case .file: return !isDirectory && !isPackage case .folder: return isDirectory && !isPackage case .package: return isPackage case .alias: return isAlias } } private func matchesTagsFilter(tagFilter: TagFilterType, tags: [String], value: String) -> Bool { let tagNames = tags.map { $0.lowercased() } switch tagFilter { case .hasTag: let searchTag = value.lowercased() return tagNames.contains(searchTag) case .hasAnyOfTags: let searchTags = value.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces).lowercased() } return searchTags.contains(where: { tagNames.contains($0) }) case .hasAllOfTags: let searchTags = value.split(separator: ",").map { $0.trimmingCharacters(in: .whitespaces).lowercased() } return searchTags.allSatisfy({ tagNames.contains($0) }) case .doesntHaveTag: let searchTag = value.lowercased() return !tagNames.contains(searchTag) } } private func matchesCommentFilter(commentFilter: CommentFilterType, comment: String, value: String) -> Bool { let commentToCompare = caseSensitive ? comment : comment.lowercased() let valueToCompare = caseSensitive ? value : value.lowercased() switch commentFilter { case .contains: return commentToCompare.contains(valueToCompare) case .doesntContain: return !commentToCompare.contains(valueToCompare) case .equals: return commentToCompare == valueToCompare case .isEmpty: return comment.isEmpty } } private func getFinderComment(for url: URL) -> String { // Finder comments are stored in the kMDItemFinderComment extended attribute guard let data = url.withUnsafeFileSystemRepresentation({ path -> Data? in guard let path = path else { return nil } let attrName = "com.apple.metadata:kMDItemFinderComment" let length = getxattr(path, attrName, nil, 0, 0, 0) guard length > 0 else { return nil } var data = Data(count: length) let result = data.withUnsafeMutableBytes { buffer in getxattr(path, attrName, buffer.baseAddress, length, 0, 0) } return result > 0 ? data : nil }) else { return "" } // Parse the property list to get the comment string if let plist = try? PropertyListSerialization.propertyList(from: data, format: nil) as? String { return plist } return "" } private func shouldExcludeSystemPath(_ path: String) -> Bool { // If we're searching a macOS root structure, check if this path is a system folder // For example: /Volumes/Macintosh HD 2/System or /System // Get the path relative to the search root let relativePath: String if path.hasPrefix(searchRootPath) { relativePath = String(path.dropFirst(searchRootPath.count)).trimmingCharacters(in: CharacterSet(charactersIn: "/")) } else { relativePath = path } // Get the first path component let components = relativePath.components(separatedBy: "/") guard let firstComponent = components.first, !firstComponent.isEmpty else { return false } // Check if the first component matches any system folder return systemFoldersToExclude.contains(firstComponent) } private func isMacOSRootStructure(_ path: String) -> Bool { // Check if this path has a typical macOS system structure // by looking for key directories: System, Users, Library, Applications let systemPath = (path as NSString).appendingPathComponent("System") let usersPath = (path as NSString).appendingPathComponent("Users") let libraryPath = (path as NSString).appendingPathComponent("Library") var isSystemDir: ObjCBool = false var isUsersDir: ObjCBool = false var isLibraryDir: ObjCBool = false let hasSystem = fileManager.fileExists(atPath: systemPath, isDirectory: &isSystemDir) && isSystemDir.boolValue let hasUsers = fileManager.fileExists(atPath: usersPath, isDirectory: &isUsersDir) && isUsersDir.boolValue let hasLibrary = fileManager.fileExists(atPath: libraryPath, isDirectory: &isLibraryDir) && isLibraryDir.boolValue // If it has at least System and Users, it's likely a macOS root return hasSystem && hasUsers && hasLibrary } } ================================================ FILE: Pearcleaner/Logic/FileSearch/FileSearchModels.swift ================================================ // // FileSearchModels.swift // Pearcleaner // // Created by Alin Lupascu on 09/29/25. // import Foundation import AppKit struct FileSearchResult: Identifiable, Hashable, Equatable { let id = UUID() let url: URL let name: String let type: String // file extension or "Folder" let size: Int64 let dateModified: Date let isDirectory: Bool let icon: NSImage? static func == (lhs: FileSearchResult, rhs: FileSearchResult) -> Bool { return lhs.id == rhs.id && lhs.url == rhs.url && lhs.name == rhs.name && lhs.type == rhs.type && lhs.size == rhs.size } func hash(into hasher: inout Hasher) { hasher.combine(id) hasher.combine(url) hasher.combine(name) hasher.combine(type) } } enum FilterType: Identifiable, Hashable { case name(NameFilterType, String) case fileExtension(ExtensionFilterType, String) // comma-separated extensions case size(SizeFilterType, Int64, Int64?) // second param is max for "between" case date(DateFilterType, Date, Date?) // second param is end date for "between" case kind(KindFilterType) case tags(TagFilterType, String) // comma-separated tags case comment(CommentFilterType, String) var id: String { switch self { case .name(let type, let value): return "name_\(type.rawValue)_\(value)" case .fileExtension(let type, let value): return "extension_\(type.rawValue)_\(value)" case .size(let type, let value, let max): return "size_\(type.rawValue)_\(value)_\(max ?? 0)" case .date(let type, let value, let end): return "date_\(type.rawValue)_\(value.timeIntervalSince1970)_\(end?.timeIntervalSince1970 ?? 0)" case .kind(let type): return "kind_\(type.rawValue)" case .tags(let type, let value): return "tags_\(type.rawValue)_\(value)" case .comment(let type, let value): return "comment_\(type.rawValue)_\(value)" } } var displayText: String { switch self { case .name(let type, let value): return "\(type.displayName): \(value)" case .fileExtension(let type, let value): return "\(type.displayName): \(value)" case .size(let type, let value, let max): if let max = max, type == .between { return "\(type.displayName): \(formatBytes(value)) - \(formatBytes(max))" } return "\(type.displayName): \(formatBytes(value))" case .date(let type, let value, let end): let formatter = DateFormatter() formatter.dateStyle = .short if let end = end, type == .createdBetween || type == .modifiedBetween { return "\(type.displayName): \(formatter.string(from: value)) - \(formatter.string(from: end))" } return "\(type.displayName): \(formatter.string(from: value))" case .kind(let type): return "Kind: \(type.displayName)" case .tags(let type, let value): return "\(type.displayName): \(value)" case .comment(let type, let value): return "\(type.displayName): \(value)" } } private func formatBytes(_ bytes: Int64) -> String { let formatter = ByteCountFormatter() formatter.allowsNonnumericFormatting = false formatter.countStyle = .binary return formatter.string(fromByteCount: bytes) } } enum NameFilterType: String, CaseIterable { case contains case doesntContain case startsWith case endsWith case equals case regex var displayName: String { switch self { case .contains: return "Name contains" case .doesntContain: return "Name doesn't contain" case .startsWith: return "Name starts with" case .endsWith: return "Name ends with" case .equals: return "Name equals" case .regex: return "Name matches regex" } } } enum ExtensionFilterType: String, CaseIterable { case includes case excludes var displayName: String { switch self { case .includes: return "Extension is" case .excludes: return "Extension is not" } } } enum SizeFilterType: String, CaseIterable { case greaterThan case lessThan case between case equals var displayName: String { switch self { case .greaterThan: return "Size greater than" case .lessThan: return "Size less than" case .between: return "Size between" case .equals: return "Size equals" } } } enum DateFilterType: String, CaseIterable { case createdBefore case createdAfter case createdBetween case modifiedBefore case modifiedAfter case modifiedBetween var displayName: String { switch self { case .createdBefore: return "Created before" case .createdAfter: return "Created after" case .createdBetween: return "Created between" case .modifiedBefore: return "Modified before" case .modifiedAfter: return "Modified after" case .modifiedBetween: return "Modified between" } } } enum KindFilterType: String, CaseIterable { case file case folder case package case alias var displayName: String { switch self { case .file: return "File" case .folder: return "Folder" case .package: return "Package" case .alias: return "Alias" } } } enum SortColumn: String, CaseIterable { case name case type case size case dateModified case path var displayName: String { switch self { case .name: return "Name" case .type: return "Type" case .size: return "Size" case .dateModified: return "Date Modified" case .path: return "Path" } } } enum SortOrder { case ascending case descending mutating func toggle() { self = self == .ascending ? .descending : .ascending } } enum TagFilterType: String, CaseIterable { case hasTag case hasAnyOfTags case hasAllOfTags case doesntHaveTag var displayName: String { switch self { case .hasTag: return "Has tag" case .hasAnyOfTags: return "Has any of tags" case .hasAllOfTags: return "Has all of tags" case .doesntHaveTag: return "Doesn't have tag" } } } enum CommentFilterType: String, CaseIterable { case contains case doesntContain case equals case isEmpty var displayName: String { switch self { case .contains: return "Comment contains" case .doesntContain: return "Comment doesn't contain" case .equals: return "Comment equals" case .isEmpty: return "Comment is empty" } } } ================================================ FILE: Pearcleaner/Logic/FuzzySearch.swift ================================================ import Foundation /// FuzzySearchCharacters is used to normalise strings struct FuzzySearchCharacter { let content: String // normalised content is referring to a string that is case- and accent-insensitive let normalisedContent: String } /// FuzzySearchString is just made up by multiple characters, similar to a string, but also with normalised characters struct FuzzySearchString { var characters: [FuzzySearchCharacter] } /// FuzzySearchMatchResult represents an object that has undergone a fuzzy search using the fuzzyMatch function. struct FuzzySearchMatchResult { let weight: Int let matchedParts: [NSRange] } extension String { /// Normalises the characters of the string by converting them to ASCII representation. /// Each character is transformed into its ASCII equivalent, and the resulting array /// of FuzzySearchCharacter objects contains both the original and normalised content. /// /// - Returns: An array of FuzzySearchCharacter objects representing the original and /// normalised content of each character in the string. func normalise() -> [FuzzySearchCharacter] { return self.lowercased().map { char in guard let data = String(char).data(using: .ascii, allowLossyConversion: true), let normalisedCharacter = String(data: data, encoding: .ascii) else { return FuzzySearchCharacter(content: String(char), normalisedContent: String(char)) } return FuzzySearchCharacter(content: String(char), normalisedContent: normalisedCharacter) } } /** Checks if the string has a prefix matching a fuzzy search character starting at a specified index. - Parameters: - prefix: A `FuzzySearchCharacter` object containing both content and normalized content for the prefix to search. - index: The index at which to start searching for the prefix within the string. - Returns: An optional integer representing the length of the matched prefix if found; otherwise, `nil`. */ func hasPrefix(prefix: FuzzySearchCharacter, startingAt index: Int) -> Int? { guard let stringIndex = self.index(self.startIndex, offsetBy: index, limitedBy: self.endIndex) else { return nil } let searchString = self.suffix(from: stringIndex) for prefix in [prefix.content, prefix.normalisedContent] where searchString.hasPrefix(prefix) { return prefix.count } return nil } } /// A protocol defining the requirements for an object that can be searched using fuzzy matching. protocol FuzzySearchable { var searchableString: String { get } /// Performs a fuzzy search on the conforming object's searchable string. /// /// - Parameters: /// - query: The query string to match against the searchable content. /// - characters: The set of characters used for fuzzy matching. /// /// - Returns: A FuzzySearchMatchResult indicating the result of the fuzzy search. func fuzzyMatch(query: String, characters: FuzzySearchString) -> FuzzySearchMatchResult } extension FuzzySearchable { func fuzzyMatch(query: String, characters: FuzzySearchString) -> FuzzySearchMatchResult { let compareString = characters.characters let searchString = query.lowercased() var totalScore = 0 var matchedParts = [NSRange]() var patternIndex = 0 var currentScore = 0 var currentMatchedPart = NSRange(location: 0, length: 0) for (index, character) in compareString.enumerated() { if let prefixLength = searchString.hasPrefix(prefix: character, startingAt: patternIndex) { patternIndex += prefixLength currentScore += 1 currentMatchedPart.length += 1 } else { currentScore = 0 if currentMatchedPart.length != 0 { matchedParts.append(currentMatchedPart) } currentMatchedPart = NSRange(location: index + 1, length: 0) } totalScore += currentScore } if currentMatchedPart.length != 0 { matchedParts.append(currentMatchedPart) } if searchString.count == matchedParts.reduce(0, { partialResult, range in range.length + partialResult }) { return FuzzySearchMatchResult(weight: totalScore, matchedParts: matchedParts) } else { return FuzzySearchMatchResult(weight: 0, matchedParts: []) } } /// Normalises the searchable string of the conforming object by converting its characters to ASCII representation. /// The resulting FuzzySearchString contains both the original and normalised content of each character. /// /// - Returns: A FuzzySearchString func normaliseString() -> FuzzySearchString { return FuzzySearchString(characters: searchableString.normalise()) } /// Performs a fuzzy search on the normalised content of the conforming object's searchable string. /// /// - Parameter query: The query string to match against the normalised searchable content. /// /// - Returns: A FuzzySearchMatchResult indicating the result of the fuzzy search. func fuzzyMatch(query: String) -> FuzzySearchMatchResult { let characters = normaliseString() return fuzzyMatch(query: query, characters: characters) } } extension Collection where Iterator.Element: FuzzySearchable { /// Asynchronously performs a fuzzy search on a collection of elements conforming to FuzzySearchable. /// /// - Parameter query: The query string to match against the elements. /// /// - Returns: An array of tuples containing FuzzySearchMatchResult and the corresponding element. /// /// - Note: Because this is an extension on Collection and not only array, /// you can also use this on sets. func fuzzySearch(query: String) -> [(result: FuzzySearchMatchResult, item: Iterator.Element)] { return map { (result: $0.fuzzyMatch(query: query), item: $0) }.filter { $0.result.weight > 0 }.sorted { $0.result.weight > $1.result.weight } } } ================================================ FILE: Pearcleaner/Logic/GlobalConsoleManager.swift ================================================ // // GlobalConsoleManager.swift // Pearcleaner // // Created by Alin Lupascu on 11/13/24. // import Foundation import SwiftUI /// Console state persisted across app launches struct ConsoleState: Codable { var isOpen: Bool var height: Double static let `default` = ConsoleState(isOpen: false, height: 200) } /// Global console manager for streaming operation output across all app pages /// Provides thread-safe console output with source tagging class GlobalConsoleManager: ObservableObject { static let shared = GlobalConsoleManager() /// Console output visible to all views @MainActor @Published var consoleOutput: String = "" /// Tracks if any operation is currently running @MainActor @Published var isOperationRunning: Bool = false /// Console visibility state (synchronized across all views) @MainActor @Published var showConsole: Bool = false /// Console height (synchronized across all views) @MainActor @Published var consoleHeight: Double = 200 private init() {} /// Append text to console output with optional source tag /// - Parameters: /// - text: Text to append /// - source: Source identifier (e.g., "Homebrew", "PKG", "Daemon") @MainActor func appendOutput(_ text: String, source: String? = nil) { let taggedText: String if let source = source, !source.isEmpty { // Only add tag if it's the start of a new line or console is empty if consoleOutput.isEmpty || consoleOutput.hasSuffix("\n") { taggedText = "[\(source)] \(text)" } else { taggedText = text } } else { taggedText = text } consoleOutput += taggedText } /// Clear all console output @MainActor func clearOutput() { consoleOutput = "" } /// Trim console to specified number of lines to prevent memory bloat /// - Parameter maxLines: Maximum number of lines to keep (default 300) @MainActor func trimOutput(toLines maxLines: Int = 300) { let lines = consoleOutput.components(separatedBy: "\n") if lines.count > maxLines { consoleOutput = lines.suffix(maxLines).joined(separator: "\n") } } } ================================================ FILE: Pearcleaner/Logic/HelperToolManager.swift ================================================ // // HelperToolManager.swift // Pearcleaner // // Created by Alin Lupascu on 3/14/25. // import ServiceManagement import AlinFoundation extension Notification.Name { static let helperRequired = Notification.Name("helperRequired") } @objc(HelperToolProtocol) public protocol HelperToolProtocol { func runCommand(command: String, withReply reply: @escaping (Bool, String) -> Void) func runThinning(atPath: String, withReply reply: @escaping (Bool, String) -> Void) func runBundleThinning(bundlePath: String, withReply reply: @escaping (Bool, String, [String: UInt64]) -> Void) } enum HelperToolAction { case none // Only check status case install // Install the helper tool case uninstall // Uninstall the helper tool case reinstall // Uninstall then reinstall (fixes desync) } class HelperToolManager: ObservableObject { static let shared = HelperToolManager() private var helperConnection: NSXPCConnection? let helperToolIdentifier = "com.alienator88.Pearcleaner.PearcleanerHelper" @Published var isHelperToolInstalled: Bool = false @Published var message: String = String(localized: "Checking...") @Published var isInitialCheckComplete: Bool = false var status: String { return isHelperToolInstalled ? String(localized:"Enabled") : String(localized:"Disabled") } var shouldShowHelperBadge: Bool { return isInitialCheckComplete && !isHelperToolInstalled } // Trigger overlay when operation fails due to missing helper func triggerHelperRequiredAlert() { NotificationCenter.default.post(name: .helperRequired, object: nil) } init() { Task { await manageHelperTool() } } // Function to manage the helper tool installation/uninstallation func manageHelperTool(action: HelperToolAction = .none) async { let plistName = "\(helperToolIdentifier).plist" let service = SMAppService.daemon(plistName: plistName) var occurredError: NSError? // Perform install/uninstall actions if specified switch action { case .install: // Pre-check before registering switch service.status { case .requiresApproval: updateOnMain { self.message = String(localized: "Registered but requires enabling in System Settings > Login Items.") } SMAppService.openSystemSettingsLoginItems() case .enabled: // Verify the helper actually works (same desync check as .none action) let whoamiResult = await runCommand("whoami", skipHelperCheck: true) let isRoot = whoamiResult.0 && whoamiResult.1.trimmingCharacters(in: .whitespacesAndNewlines) == "root" if !isRoot { // Desync detected - helper claims enabled but doesn't work printOS("Helper desync detected during install attempt, attempting auto-recovery...") updateOnMain { self.message = String(localized: "Service desynced, attempting recovery...") } // Recursively call with reinstall action await manageHelperTool(action: .reinstall) return // Exit early, reinstall will handle status updates } // Helper verified working updateOnMain { self.message = String(localized: "Service is already enabled.") } default: do { try service.register() if service.status == .requiresApproval { SMAppService.openSystemSettingsLoginItems() } } catch let nsError as NSError { occurredError = nsError if nsError.code == 1 { // Operation not permitted updateOnMain { self.message = String(localized: "Permission required. Enable in System Settings > Login Items.") } SMAppService.openSystemSettingsLoginItems() } else { updateOnMain { self.message = String(localized: "Installation failed: \(nsError.localizedDescription)") } printOS("Failed to register helper: \(nsError.localizedDescription)") } } } case .uninstall: do { try await service.unregister() // Close any existing connection helperConnection?.invalidate() helperConnection = nil } catch let nsError as NSError { occurredError = nsError printOS("Failed to unregister helper: \(nsError.localizedDescription)") } case .reinstall: // Uninstall first do { try await service.unregister() helperConnection?.invalidate() helperConnection = nil } catch let nsError as NSError { printOS("Reinstall: Failed to unregister: \(nsError.localizedDescription)") } // Small delay to ensure launchd processes the unregister try? await Task.sleep(nanoseconds: 500_000_000) // 0.5 seconds // Then install do { try service.register() if service.status == .requiresApproval { SMAppService.openSystemSettingsLoginItems() } } catch let nsError as NSError { occurredError = nsError printOS("Reinstall: Failed to register: \(nsError.localizedDescription)") } case .none: break } await updateStatusMessages(with: service, occurredError: occurredError) let isEnabled = (service.status == .enabled) // let whoamiResult = await runCommand("whoami", skipHelperCheck: true) // let isRoot = whoamiResult.0 && whoamiResult.1.trimmingCharacters(in: .whitespacesAndNewlines) == "root" updateOnMain { self.isHelperToolInstalled = isEnabled// && isRoot self.isInitialCheckComplete = true } } // Function to open Settings > Login Items func openSMSettings() { SMAppService.openSystemSettingsLoginItems() } // Function to run privileged commands func runCommand(_ command: String, skipHelperCheck: Bool = false) async -> (Bool, String) { if !skipHelperCheck && !isHelperToolInstalled { return (false, "XPC: Helper tool is not installed") } guard let connection = getConnection() else { return (false, "XPC: Connection not available") } return await withCheckedContinuation { continuation in guard let proxy = connection.remoteObjectProxyWithErrorHandler({ error in continuation.resume(returning: (false, "XPC: Connection error: \(error.localizedDescription)")) }) as? HelperToolProtocol else { continuation.resume(returning: (false, "XPC: Failed to get remote object")) return } proxy.runCommand(command: command, withReply: { success, output in continuation.resume(returning: (success, output)) }) } } // Function to run privileged thinning on apps owned by root func runThinning(atPath path: String) async -> (Bool, String) { guard let connection = getConnection() else { return (false, "XPC: No helper connection") } return await withCheckedContinuation { continuation in guard let proxy = connection.remoteObjectProxyWithErrorHandler({ error in continuation.resume(returning: (false, "XPC: Error: \(error.localizedDescription)")) }) as? HelperToolProtocol else { continuation.resume(returning: (false, "XPC: Proxy failure")) return } proxy.runThinning(atPath: path) { success, output in continuation.resume(returning: (success, output)) } } } // Function to run privileged bundle thinning on entire app bundles func runBundleThinning(bundlePath path: String) async -> (Bool, String, [String: UInt64]) { guard let connection = getConnection() else { return (false, "XPC: No helper connection", [:]) } return await withCheckedContinuation { continuation in guard let proxy = connection.remoteObjectProxyWithErrorHandler({ error in continuation.resume(returning: (false, "XPC: Error: \(error.localizedDescription)", [:])) }) as? HelperToolProtocol else { continuation.resume(returning: (false, "XPC: Proxy failure", [:])) return } proxy.runBundleThinning(bundlePath: path) { success, output, sizes in continuation.resume(returning: (success, output, sizes)) } } } // Create/reuse XPC connection private func getConnection() -> NSXPCConnection? { if let connection = helperConnection { return connection } let connection = NSXPCConnection(machServiceName: helperToolIdentifier, options: .privileged) connection.remoteObjectInterface = NSXPCInterface(with: HelperToolProtocol.self) connection.invalidationHandler = { [weak self] in self?.helperConnection = nil } connection.resume() helperConnection = connection return connection } // Helper to update helper status messages func updateStatusMessages(with service: SMAppService, occurredError: NSError?) async { if let nsError = occurredError { switch nsError.code { case kSMErrorAlreadyRegistered: updateOnMain { self.message = String(localized: "Service is already registered and enabled.") } case kSMErrorLaunchDeniedByUser: updateOnMain { self.message = String(localized: "User denied permission. Enable in System Settings > Login Items.") } case kSMErrorInvalidSignature: updateOnMain { self.message = String(localized: "Invalid signature, ensure proper signing on the application and helper tool.") } case 1: updateOnMain { self.message = String(localized: "Authorization required in Settings > Login Items > \(Bundle.main.name).app.") } default: updateOnMain { self.message = String(localized: "Operation failed: \(nsError.localizedDescription)") } } } else { switch service.status { case .notRegistered: updateOnMain { self.message = String(localized: "Service hasn't been registered. You may register it now.") } case .enabled: let whoamiResult = await runCommand("whoami", skipHelperCheck: true) let isRoot = whoamiResult.0 && whoamiResult.1.trimmingCharacters(in: .whitespacesAndNewlines) == "root" if !isRoot { // Desync detected - try reinstall first, then nuclear reset updateOnMain { self.message = String(localized: "Service desynced, attempting recovery...") } // Try standard reinstall first await manageHelperTool(action: .reinstall) return // Exit early, operations will handle status updates } updateOnMain { self.message = String(localized: "Service successfully registered and eligible to run.") } case .requiresApproval: updateOnMain { self.message = String(localized: "Service registered but requires user approval in Settings > Login Items > \(Bundle.main.name).app.") } case .notFound: updateOnMain { self.message = String(localized: "Service is not installed.") } @unknown default: updateOnMain { self.message = String(localized: "Unknown service status (\(service.status.rawValue)).") } } } } // MARK: - Nuclear Reset /// Nuclear reset: Reset BTM (Background Task Management) database to clear desynced helper registrations /// This is a last-resort fix for when SMAppService becomes desynced during development /// PREREQUISITE: User must manually disable helper in System Settings > Login Items first in certain cases /// Uses AlinFoundation's performPrivilegedCommands() which prompts for password func nuclearResetHelper() async -> Bool { printOS("Starting nuclear reset of helper tool...") updateOnMain { self.message = String(localized: "Resetting BTM database...") } // Execute sfltool resetbtm to clear Background Task Management database // NOTE: This only works if user has disabled the service in System Settings first let (success, output) = performPrivilegedCommands(commands: "sfltool resetbtm") if success { printOS("BTM reset succeeded") // Invalidate XPC connection helperConnection?.invalidate() helperConnection = nil updateOnMain { self.message = String(localized: "BTM reset complete. Please reinstall helper.") self.isHelperToolInstalled = false } return true } else { printOS("BTM reset failed: \(output)") updateOnMain { self.message = String(localized: "BTM reset failed: \(output)") } return false } } } ================================================ FILE: Pearcleaner/Logic/KeychainPasswordManager.swift ================================================ // // KeychainPasswordManager.swift // Pearcleaner // // Manages sudo password caching in macOS Keychain with time-based expiry // Created by Alin Lupascu on 11/10/24. // import Foundation import Security class KeychainPasswordManager { static let shared = KeychainPasswordManager() private let service = "com.alienator88.Pearcleaner.SudoPassword" private let account: String private init() { self.account = NSUserName() } enum KeychainError: Error { case invalidData case itemNotFound case unexpectedStatus(OSStatus) } // MARK: - Public API /// Reads user's configured cache timeout from UserDefaults static func getCacheTimeout() -> TimeInterval { guard let data = UserDefaults.standard.data(forKey: "settings.general.sudoCacheTimeout"), let decoded = try? JSONDecoder().decode(SudoCacheTimeoutSetting.self, from: data) else { return 300 // Default to 5 minutes } return decoded.seconds } /// Saves password to keychain with expiry time stored in metadata func savePassword(_ password: String, expiryInterval: TimeInterval? = nil) { let timeout = expiryInterval ?? KeychainPasswordManager.getCacheTimeout() // Delete existing item first deletePassword(service: service, account: account) // Calculate expiry timestamp and store as metadata let expiryDate = Date().addingTimeInterval(timeout) let expiryTimestamp = expiryDate.timeIntervalSince1970 let expiryString = "\(expiryTimestamp)" guard let passwordData = password.data(using: .utf8), let expiryData = expiryString.data(using: .utf8) else { return } let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account, kSecValueData as String: passwordData, kSecAttrGeneric as String: expiryData, // Store expiry in generic attribute kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlocked ] SecItemAdd(query as CFDictionary, nil) } /// Retrieves password from keychain if not expired func retrievePassword() -> String? { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account, kSecReturnData as String: kCFBooleanTrue!, kSecReturnAttributes as String: kCFBooleanTrue!, // Also return attributes kSecMatchLimit as String: kSecMatchLimitOne ] var item: CFTypeRef? let status = SecItemCopyMatching(query as CFDictionary, &item) guard status == errSecSuccess, let existingItem = item as? [String: Any], let passwordData = existingItem[kSecValueData as String] as? Data, let password = String(data: passwordData, encoding: .utf8), let expiryData = existingItem[kSecAttrGeneric as String] as? Data, let expiryString = String(data: expiryData, encoding: .utf8), let expiryTimestamp = TimeInterval(expiryString) else { return nil } let expiryDate = Date(timeIntervalSince1970: expiryTimestamp) if Date() > expiryDate { invalidateCache() return nil } return password } /// Removes password from keychain func invalidateCache() { deletePassword(service: service, account: account) } // MARK: - Private Keychain Operations private func deletePassword(service: String, account: String) { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: service, kSecAttrAccount as String: account ] SecItemDelete(query as CFDictionary) } } // MARK: - SudoCacheTimeoutSetting /// Mirrors the SudoCacheTimeout struct from General.swift for decoding struct SudoCacheTimeoutSetting: Codable { var value: Int = 5 var unit: TimeUnit = .minutes enum TimeUnit: String, Codable { case minutes = "Minutes" case hours = "Hours" case days = "Days" } var seconds: TimeInterval { switch unit { case .minutes: return TimeInterval(value * 60) case .hours: return TimeInterval(value * 3600) case .days: return TimeInterval(value * 86400) } } } ================================================ FILE: Pearcleaner/Logic/Lipo.swift ================================================ // // Lipo.swift // Pearcleaner // // Created by Alin Lupascu on 4/10/25. // import Foundation // Helper structs for Mach-O parsing public struct FatHeader { public let magic: UInt32 public let numArchitectures: UInt32 public init(magic: UInt32, numArchitectures: UInt32) { self.magic = magic self.numArchitectures = numArchitectures } } public struct FatArch { public let cpuType: UInt32 public let cpuSubtype: UInt32 public let offset: UInt32 public let size: UInt32 public let align: UInt32 public init(cpuType: UInt32, cpuSubtype: UInt32, offset: UInt32, size: UInt32, align: UInt32) { self.cpuType = cpuType self.cpuSubtype = cpuSubtype self.offset = offset self.size = size self.align = align } } // Function to thin an entire app bundle with size tracking public func thinAppBundle(at bundlePath: URL, dryRun: Bool = false) -> (Bool, [String: UInt64]?) { // Get the total bundle size before thinning let preTotalSize = UInt64(totalSizeOnDisk(for: bundlePath)) let result = recursivelyThinBundle(at: bundlePath, dryRun: dryRun) if result.success { if dryRun { // For dry run, calculate estimated post-size based on binary savings let binarySavings = result.sizes?["binarySavings"] ?? 0 let estimatedPostSize = preTotalSize > binarySavings ? preTotalSize - binarySavings : preTotalSize let sizes = ["pre": preTotalSize, "post": estimatedPostSize] return (true, sizes) } else { // For real thinning, get the actual bundle size after thinning let postTotalSize = UInt64(totalSizeOnDisk(for: bundlePath)) let sizes = ["pre": preTotalSize, "post": postTotalSize] return (true, sizes) } } else { return (result.success, result.sizes) } } // Recursively thin all binaries in a bundle func recursivelyThinBundle(at path: URL, dryRun: Bool = false) -> (success: Bool, sizes: [String: UInt64]?) { let fileManager = FileManager.default guard let enumerator = fileManager.enumerator(at: path, includingPropertiesForKeys: [.isDirectoryKey, .isExecutableKey], options: [.skipsHiddenFiles]) else { print("Bundle Error: Could not enumerate bundle contents") return (false, nil) } var processedFiles: [String] = [] var skippedFiles: [String] = [] var totalPreSize: UInt64 = 0 var totalPostSize: UInt64 = 0 // Collect all file URLs first to avoid keeping enumerator handles open var candidateFiles: [URL] = [] autoreleasepool { for case let fileURL as URL in enumerator { autoreleasepool { // Skip directories early let resourceValues = try? fileURL.resourceValues(forKeys: [.isDirectoryKey]) if resourceValues?.isDirectory == true { return } candidateFiles.append(fileURL) } } } // Process files in batches to limit memory and file descriptor usage let batchSize = 50 for batchStart in stride(from: 0, to: candidateFiles.count, by: batchSize) { autoreleasepool { let batchEnd = min(batchStart + batchSize, candidateFiles.count) let batch = Array(candidateFiles[batchStart.. removedSize ? preSize - removedSize : preSize totalPostSize += estimatedPostSize processedFiles.append(fileURL.path) } else { skippedFiles.append(fileURL.path) } } catch { skippedFiles.append(fileURL.path) } } else { // Real thinning if thinBinaryUsingMachO(executablePath: fileURL.path) { // Get file size after thinning if let postAttributes = try? fileManager.attributesOfItem(atPath: fileURL.path), let postSize = postAttributes[.size] as? UInt64 { totalPreSize += preSize totalPostSize += postSize } processedFiles.append(fileURL.path) } else { skippedFiles.append(fileURL.path) } } } else { skippedFiles.append(fileURL.path) } } } } } } let success = processedFiles.count > 0 || (processedFiles.count == 0 && skippedFiles.count >= 0) var sizes: [String: UInt64]? = nil if totalPreSize > 0 { if dryRun { // For dry run, include the binary savings calculation let binarySavings = totalPreSize > totalPostSize ? totalPreSize - totalPostSize : 0 sizes = ["pre": totalPreSize, "post": totalPostSize, "binarySavings": binarySavings] } else { // For real thinning, just include pre/post sizes = ["pre": totalPreSize, "post": totalPostSize] } } return (success, sizes) } // Determine if a file should be thinned func shouldThinFile(_ url: URL) -> Bool { // Check if it's an executable binary return isExecutableBinary(url) } // Find the app bundle path by traversing up the directory tree func findAppBundlePath(from url: URL) -> URL { var currentURL = url // Keep going up until we find a .app bundle or reach the root while currentURL.path != "/" { if currentURL.pathExtension == "app" { return currentURL } currentURL = currentURL.deletingLastPathComponent() } // Fallback: assume it's a traditional app bundle structure var fallbackURL = url while fallbackURL.path != "/" && !fallbackURL.path.hasSuffix(".app") { fallbackURL = fallbackURL.deletingLastPathComponent() } return fallbackURL } // Check if a file is an executable binary public func isExecutableBinary(_ url: URL) -> Bool { // First check file extension for known binary types let pathExtension = url.pathExtension.lowercased() let knownBinaryExtensions = ["dylib", "so", "bundle"] // If it's a known binary extension, assume it's a binary (faster than reading file) if knownBinaryExtensions.contains(pathExtension) { return true } // Special handling for bundle structures that might contain binaries // (.appex, .xpc, .framework are bundles, but we want to check their executables inside) let bundleExtensions = ["appex", "xpc", "framework"] if bundleExtensions.contains(pathExtension) { // These are bundles - the enumerator will traverse into them // and find the actual executable inside return false } // For other files, check magic numbers - use FileHandle to read only 4 bytes guard let fileHandle = try? FileHandle(forReadingFrom: url) else { return false } defer { try? fileHandle.close() } guard let magicData = try? fileHandle.read(upToCount: 4), magicData.count == 4 else { return false } let magic = magicData.withUnsafeBytes { $0.load(as: UInt32.self) } let FAT_MAGIC: UInt32 = 0xcafebabe let FAT_MAGIC_SWAPPED: UInt32 = 0xbebafeca // Little-endian version let MH_MAGIC_64: UInt32 = 0xfeedfacf let MH_CIGAM_64: UInt32 = 0xcffaedfe let MH_MAGIC: UInt32 = 0xfeedface let MH_CIGAM: UInt32 = 0xcefaedfe return magic == FAT_MAGIC || magic == FAT_MAGIC_SWAPPED || magic == MH_MAGIC_64 || magic == MH_CIGAM_64 || magic == MH_MAGIC || magic == MH_CIGAM } // Helper function to thin a binary using Mach-O APIs public func thinBinaryUsingMachO(executablePath: String) -> Bool { // Determine the target architecture based on the current OS var targetArch: String #if arch(arm64) targetArch = "arm64" #else targetArch = "x86_64" #endif // Find the app bundle path by searching up the directory tree let executableURL = URL(fileURLWithPath: executablePath) let appBundlePath = findAppBundlePath(from: executableURL) return autoreleasepool { do { // Use FileHandle instead of loading entire file into memory guard let fileHandle = try? FileHandle(forReadingFrom: URL(fileURLWithPath: executablePath)) else { return false } defer { try? fileHandle.close() } // Read just the header to check if it's a fat binary guard let headerData = try? fileHandle.read(upToCount: 8), headerData.count == 8 else { return false } let FAT_MAGIC: UInt32 = 0xcafebabe let fatHeader = headerData.withUnsafeBytes { ptr in FatHeader( magic: ptr.load(fromByteOffset: 0, as: UInt32.self).bigEndian, numArchitectures: ptr.load(fromByteOffset: 4, as: UInt32.self).bigEndian ) } guard fatHeader.magic == FAT_MAGIC else { // printOS("Mach-O Error: Not a universal binary, skipping thinning.") return false } var offset = 8 var foundArch: FatArch? for _ in 0.. (arm: UInt32, intel: UInt32, full: UInt32)? { guard !executablePath.isEmpty else { throw NSError(domain: "LipoError", code: 1, userInfo: [NSLocalizedDescriptionKey: "Empty executable path"]) } return try autoreleasepool { let fileURL = URL(fileURLWithPath: executablePath) // Use FileHandle to read only necessary bytes guard let fileHandle = try? FileHandle(forReadingFrom: fileURL) else { throw NSError(domain: "LipoError", code: 2, userInfo: [NSLocalizedDescriptionKey: "Could not open file"]) } defer { try? fileHandle.close() } // Get file size efficiently using FileHandle let fileSize = fileHandle.seekToEndOfFile() let fullSize = UInt32(min(fileSize, UInt64(UInt32.max))) // Seek back to beginning to read header try fileHandle.seek(toOffset: 0) guard let headerData = try? fileHandle.read(upToCount: 8), headerData.count >= 8 else { throw NSError(domain: "LipoError", code: 3, userInfo: [NSLocalizedDescriptionKey: "File too small to contain valid header"]) } let FAT_MAGIC: UInt32 = 0xcafebabe let header = try headerData.withUnsafeBytes { ptr -> FatHeader in guard ptr.count >= 8 else { throw NSError(domain: "LipoError", code: 4, userInfo: [NSLocalizedDescriptionKey: "Insufficient data for header"]) } return FatHeader( magic: ptr.load(fromByteOffset: 0, as: UInt32.self).bigEndian, numArchitectures: ptr.load(fromByteOffset: 4, as: UInt32.self).bigEndian ) } var armSize: UInt32 = 0 var intelSize: UInt32 = 0 if header.magic == FAT_MAGIC { guard header.numArchitectures > 0 && header.numArchitectures < 100 else { throw NSError(domain: "LipoError", code: 5, userInfo: [NSLocalizedDescriptionKey: "Invalid number of architectures"]) } // Read all architecture headers at once (20 bytes each) let archDataSize = Int(header.numArchitectures) * 20 guard let allArchData = try? fileHandle.read(upToCount: archDataSize), allArchData.count == archDataSize else { throw NSError(domain: "LipoError", code: 6, userInfo: [NSLocalizedDescriptionKey: "Could not read architecture headers"]) } var offset = 0 for _ in 0.. FatArch in FatArch( cpuType: ptr.load(fromByteOffset: 0, as: UInt32.self).bigEndian, cpuSubtype: ptr.load(fromByteOffset: 4, as: UInt32.self).bigEndian, offset: ptr.load(fromByteOffset: 8, as: UInt32.self).bigEndian, size: ptr.load(fromByteOffset: 12, as: UInt32.self).bigEndian, align: ptr.load(fromByteOffset: 16, as: UInt32.self).bigEndian ) } if arch.cpuType == 0x100000C { armSize = arch.size } else if arch.cpuType == 0x01000007 { intelSize = arch.size } offset += 20 } } else { // For a lipo'd binary, assume the whole file is the slice. // Read bytes 4-8 to check CPU type try? fileHandle.seek(toOffset: 4) guard let cpuTypeData = try? fileHandle.read(upToCount: 4), cpuTypeData.count == 4 else { return (arm: 0, intel: 0, full: fullSize) } let cpuType = cpuTypeData.withUnsafeBytes { $0.load(as: UInt32.self).bigEndian } if cpuType == 0x100000C { armSize = fullSize } else if cpuType == 0x01000007 { intelSize = fullSize } } return (arm: armSize, intel: intelSize, full: fullSize) } } // Get size of files (logical size - matches Finder) public func totalSizeOnDisk(for paths: [URL]) -> Int64 { let fileManager = FileManager.default var totalFileSize: Int64 = 0 for url in paths { var isDirectory: ObjCBool = false if fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) { let keys: [URLResourceKey] = [.fileSizeKey] if isDirectory.boolValue { //MARK: Directory Size if let enumerator = fileManager.enumerator(at: url, includingPropertiesForKeys: keys, errorHandler: nil) { for case let fileURL as URL in enumerator { do { if let size = (try? fileURL.resourceValues(forKeys: [.fileSizeKey]))?.fileSize { totalFileSize += Int64(size) } } } } } else { //MARK: File Size do { if let size = (try? url.resourceValues(forKeys: [.fileSizeKey]))?.fileSize { totalFileSize += Int64(size) } } } } } return totalFileSize } public func totalSizeOnDisk(for path: URL) -> Int64 { return totalSizeOnDisk(for: [path]) } ================================================ FILE: Pearcleaner/Logic/Locations.swift ================================================ // // Locations.swift // Pearcleaner // // Created by Alin Lupascu on 11/10/23. // import Foundation class Locations: ObservableObject { struct Category { let name: String var paths: [String] } struct PluginCategory { let name: String var subcategories: [String: [String]] } // Standard macOS Library subdirectories // Used to determine if depth=2 matches should add parent directory (vendor folders) // or the matched item itself (standard system folders) static let standardLibrarySubdirectories: Set = [ "Application Scripts", "Application Support", "Caches", "Containers", "Group Containers", "HTTPStorages", "Internet Plug-Ins", "LaunchAgents", "LaunchDaemons", "Logs", "Preferences", "PreferencePanes", "PrivilegedHelperTools", "Saved Application State", "Services", "WebKit", "Extensions", "Frameworks" ] let cacheDir: String let tempDir: String var apps: Category var reverse: Category var plugins: PluginCategory init() { let (cacheDir, tempDir) = darwinCT() self.cacheDir = cacheDir self.tempDir = tempDir self.apps = Category(name: "Apps", paths: [ "\(home)", "\(home)/.config", "\(home)/Documents", "\(home)/Desktop", // for steam game shortcuts "\(home)/Applications", "\(home)/Library", "\(home)/Library/Application Scripts", "\(home)/Library/Application Support", "\(home)/Library/Application Support/CrashReporter", "\(home)/Library/Application Support/Steam/steamapps", "\(home)/Library/Application Support/Steam/steamapps/common", "\(home)/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments", "\(home)/Library/Containers", "\(home)/Library/Caches", "\(home)/Library/Caches/com.apple.helpd/Generated", "\(home)/Library/Caches/com.crashlytics", "\(home)/Library/Caches/com.google.SoftwareUpdate", "\(home)/Library/Caches/com.google.Keystone", "\(home)/Library/Caches/org.sparkle-project.Sparkle", "\(home)/Library/Caches/com.segment.analytics", "\(home)/Library/Caches/SentryCrash", "\(home)/Library/Caches/Rollbar", "\(home)/Library/Caches/Amplitude", "\(home)/Library/Caches/Realm", "\(home)/Library/Caches/Parse", "\(home)/Library/Group Containers", "\(home)/Library/HTTPStorages", "\(home)/Library/Internet Plug-Ins", "\(home)/Library/LaunchAgents", "\(home)/Library/Logs", "\(home)/Library/Logs/DiagnosticReports", "\(home)/Library/Preferences", "\(home)/Library/PreferencePanes", "\(home)/Library/Preferences/ByHost", "\(home)/Library/Saved Application State", "\(home)/Library/Services", "\(home)/Library/WebKit", "/Applications", "/Users/Shared", "/Users/Library", "/Users/Shared/Library/Application Support", "/Library", "/Library/Application Support", "/Library/Application Support/CrashReporter", "/Library/Caches", "/Library/Extensions", "/Library/Internet Plug-Ins", "/Library/LaunchAgents", "/Library/LaunchDaemons", "/Library/Logs", "/Library/Logs/DiagnosticReports", "/Library/Preferences", "/Library/PrivilegedHelperTools", "/private/var/db/receipts", "/private/tmp", "/usr/local/bin", "/usr/local/etc", "/usr/local/opt", "/usr/local/sbin", "/usr/local/share", "/usr/local/var", cacheDir, tempDir ]) // Append Application Support subfolders for deeper search let subfolders = listAppSupportDirectories() for folder in subfolders { self.apps.paths.append("\(home)/Library/Application Support/\(folder)") } self.reverse = Category(name: "Reverse", paths: [ "\(home)/Library/Application Scripts", "\(home)/Library/Application Support", "\(home)/Library/Application Support/Caches", "\(home)/Library/Application Support/com.apple.sharedfilelist/com.apple.LSSharedFileList.ApplicationRecentDocuments", "\(home)/Library/Containers", "\(home)/Library/Caches", "\(home)/Library/HTTPStorages", "\(home)/Library/Internet Plug-Ins", "\(home)/Library/LaunchAgents", "\(home)/Library/Logs", "\(home)/Library/Preferences", "\(home)/Library/PreferencePanes", "\(home)/Library/Preferences/ByHost", "\(home)/Library/Saved Application State", "\(home)/Library/WebKit", "/Users/Shared/Library/Application Support", "/Library/Application Support", "/Library/Application Support/CrashReporter", "/Library/Internet Plug-Ins", "/Library/LaunchAgents", "/Library/LaunchDaemons", "/Library/PrivilegedHelperTools", ]) self.plugins = PluginCategory(name: "Plugins", subcategories: [ "Audio": [ // Audio Components, VST, AU, etc. "\(home)/Library/Audio/Plug-Ins/Components", "\(home)/Library/Audio/Plug-Ins/HAL", "\(home)/Library/Audio/Plug-Ins/MAS", "\(home)/Library/Audio/Plug-Ins/VST", "\(home)/Library/Audio/Plug-Ins/VST3", "\(home)/Library/Audio/Plug-Ins/CLAP", "/Library/Audio/Plug-Ins/HAL", "/Library/Audio/Plug-Ins/VST", "/Library/Audio/Plug-Ins/VST3", "/Library/Audio/Plug-Ins/CLAP", "/Library/Audio/Plug-Ins/Components", "/Library/Application Support/Avid/Audio/Plug-Ins", "/Library/Application Support/Digidesign/Plug-Ins" ], "PreferencePanes": [ // System Preference Panes (.prefPane) "/Library/PreferencePanes", "\(home)/Library/PreferencePanes" ], "QuickLook": [ // QuickLook Generators (.qlgenerator) "/Library/QuickLook", "\(home)/Library/QuickLook" ], "Screen Savers": [ // Screen Savers (.saver) "/Library/Screen Savers", "\(home)/Library/Screen Savers" ], "Internet Plug-Ins": [ // Browser plugins (.plugin, .webplugin) "/Library/Internet Plug-Ins", "\(home)/Library/Internet Plug-Ins" ], "Core Image": [ // Core Image filters (.plugin) "/Library/CoreImage", "\(home)/Library/CoreImage" ], "ColorPickers": [ // Color Picker plugins (.colorPicker) "/Library/ColorPickers", "\(home)/Library/ColorPickers" ], "Fonts": [ // Font files (.ttf, .otf, .dfont, .ttc) "\(home)/Library/Fonts", ], "Dictionaries": [ // Dictionary files (.dictionary) "/Library/Dictionaries", "\(home)/Library/Dictionaries" ], "Automator": [ // Automator Actions (.action, .workflow) "/Library/Automator", "\(home)/Library/Automator" ], "Safari Extensions": [ // Safari Extensions (.safariextz, .appex) "/Library/Safari/Extensions", "\(home)/Library/Safari/Extensions" ], "Motion Templates": [ // Final Cut Pro and Motion templates "\(home)/Movies/Motion Templates", "/Library/Application Support/Final Cut Pro System Support/Plug-ins" ], "Spotlight": [ // Spotlight importers (.mdimporter) "/Library/Spotlight", "\(home)/Library/Spotlight" ], "Services": [ // System Services (.service) "/Library/Services", "\(home)/Library/Services" ], "Address Book": [ // Address Book plugins "\(home)/Library/Address Book Plug-Ins" ], "Contextual Menu": [ // Context menu plugins "/Library/Contextual Menu Items", "\(home)/Library/Contextual Menu Items" ], "Input Methods": [ // Input method editors "/Library/Input Methods", "\(home)/Library/Input Methods" ], "Widgets": [ // Dashboard and notification widgets (.wdgt, .appex) "/Library/Widgets", "\(home)/Library/Widgets" ] ]) } } ================================================ FILE: Pearcleaner/Logic/Logic.swift ================================================ // // Logic.swift // Pearcleaner // // Created by Alin Lupascu on 10/31/23. // import AlinFoundation import Foundation import ServiceManagement import SwiftUI import UniformTypeIdentifiers // MARK: - String Sorting Extension extension String { /// Returns a normalized sort key that handles Chinese characters via pinyin transformation /// Only applies expensive transformation when CJK characters are detected var sortKey: String { // Check if string contains CJK characters let containsCJK = self.unicodeScalars.contains { scalar in (0x4E00...0x9FFF).contains(scalar.value) || // CJK Unified Ideographs (0x3400...0x4DBF).contains(scalar.value) || // CJK Extension A (0x20000...0x2A6DF).contains(scalar.value) // CJK Extension B } if containsCJK { // Apply pinyin transformation for Chinese characters let latin = self.applyingTransform(.toLatin, reverse: false) ?? self let noTone = latin.applyingTransform(.stripDiacritics, reverse: false) ?? latin return noTone.lowercased() } else { // Fast path for non-CJK strings return self.lowercased() } } } /// Creates optimally-sized chunks for parallel processing based on system capabilities /// - Parameters: /// - array: The array to chunk /// - minChunkSize: Minimum size per chunk (default: 10) /// - maxChunkSize: Maximum size per chunk (default: 50) /// - Returns: Array of chunks optimized for the current system func createOptimalChunks(from array: [T], minChunkSize: Int = 10, maxChunkSize: Int = 50) -> [[T]] { let coreCount = ProcessInfo.processInfo.activeProcessorCount let chunkSize = min(max(array.count / coreCount, minChunkSize), maxChunkSize) return array.chunked(into: chunkSize) } /// Flush bundle caches for the given app paths to ensure fresh version info /// - Parameter apps: Array of AppInfo objects whose bundles should have caches flushed /// - Discussion: (NS)Bundle caches Info.plist data. After app updates, old version info may be /// returned. Flushing the cache using private API ensures current data is read. func flushBundleCaches(for apps: [AppInfo]) { for app in apps { autoreleasepool { guard let bundle = Bundle(url: app.path) else { return } if let bundleRef = CFBundleCreate(nil, bundle.bundleURL as CFURL) { _CFBundleFlushBundleCaches(bundleRef) } } } } /// Flush bundle cache for a specific path (without requiring AppInfo object) /// Used when loading newly installed apps where bundle cache might be stale func flushBundleCache(for path: URL) { autoreleasepool { if let bundleRef = CFBundleCreate(nil, path as CFURL) { _CFBundleFlushBundleCaches(bundleRef) } } } /// Load apps from specified folder paths and update AppState /// This is the main entry point for loading/refreshing apps /// Apps stream into AppState.shared.sortedApps progressively as chunks complete /// - Parameter useStreaming: If true, uses two-phase streaming (fast initial load). If false, loads full AppInfo immediately. func loadApps(folderPaths: [String], useStreaming: Bool = false) { // Clear array immediately before loading Task { @MainActor in AppState.shared.sortedApps = [] } DispatchQueue.global(qos: .userInitiated).async { // getSortedApps now streams results to AppState.shared.sortedApps (or returns full array if not streaming) let apps = getSortedApps(paths: folderPaths, useStreaming: useStreaming) // If not streaming, update AppState with sorted results if !useStreaming { Task { @MainActor in AppState.shared.sortedApps = apps } } Task { @MainActor in AppState.shared.restoreZombieAssociations() } } } // Awaitable version that waits for apps to finish loading // Note: With streaming, this still clears and starts loading but doesn't wait for completion func loadAppsAsync(folderPaths: [String], useStreaming: Bool = false) async { // Clear array immediately before loading await MainActor.run { AppState.shared.sortedApps = [] } // Load apps on background thread (streams results or returns full array) let apps = await Task.detached(priority: .userInitiated) { getSortedApps(paths: folderPaths, useStreaming: useStreaming) }.value // If not streaming, update AppState with sorted results if !useStreaming { await MainActor.run { AppState.shared.sortedApps = apps } } // Update AppState on MainActor await MainActor.run { AppState.shared.restoreZombieAssociations() } } // Get all apps from /Applications and ~/Applications /// - Parameter useStreaming: If true, uses two-phase streaming (AppInfoMini → full AppInfo). If false, loads full AppInfo immediately. /// - Returns: Array of AppInfo. Empty if streaming (results delivered via AppState updates), populated if not streaming. func getSortedApps(paths: [String], useStreaming: Bool = false) -> [AppInfo] { @AppStorage("settings.updater.loadOnStartup") var loadUpdatesOnStartup: Bool = true let fileManager = FileManager.default var apps: [URL] = [] func collectAppPaths(at directoryPath: String) { let queue = DispatchQueue(label: "com.pearcleaner.filetree", qos: .userInitiated, attributes: .concurrent) let group = DispatchGroup() let appsQueue = DispatchQueue(label: "com.pearcleaner.apps.collection") func collectAppPathsParallel(at directoryPath: String) { do { let appURLs = try fileManager.contentsOfDirectory( at: URL(fileURLWithPath: directoryPath), includingPropertiesForKeys: [.isDirectoryKey, .isSymbolicLinkKey], options: []) var foundApps: [URL] = [] var subdirectories: [URL] = [] // Separate apps from subdirectories in one pass for appURL in appURLs { let resourceValues = try? appURL.resourceValues(forKeys: [.isDirectoryKey, .isSymbolicLinkKey]) let isDirectory = resourceValues?.isDirectory ?? false let isSymlink = resourceValues?.isSymbolicLink ?? false if appURL.pathExtension == "app" && !isRestricted(atPath: appURL) && !isSymlink { foundApps.append(appURL) } else if isDirectory && !isSymlink { subdirectories.append(appURL) } } // Add found apps to the main collection if !foundApps.isEmpty { appsQueue.sync { apps.append(contentsOf: foundApps) } } // Process subdirectories in parallel for subdirectory in subdirectories { group.enter() queue.async { collectAppPathsParallel(at: subdirectory.path) group.leave() } } } catch { printOS("Error: \(error)") } } // Start the parallel collection group.enter() queue.async { collectAppPathsParallel(at: directoryPath) group.leave() } group.wait() } // Collect system applications paths.forEach { path in if fileManager.fileExists(atPath: path) { collectAppPaths(at: path) } } // === DEBUG: Duplicate AppCleaner for testing high app counts === #if DEBUG // if let appCleanerURL = apps.first(where: { $0.lastPathComponent == "AppCleaner.app" }) { // print("🧪 TEST MODE: Duplicating AppCleaner 150 times for stress testing") // // Simply add the same URL 150 times - they'll get unique UUIDs but share metadata // for _ in 1...150 { // apps.append(appCleanerURL) // } // print("🧪 Total apps after duplication: \(apps.count)") // } #endif // === END DEBUG === // Convert collected paths to string format for metadata query let combinedPaths = apps.map { $0.path } // Get metadata for all collected app paths var metadataDictionary: [String: [String: Any]] = [:] if let metadata = getMDLSMetadata(for: combinedPaths) { metadataDictionary = metadata } if useStreaming { // === STREAMING MODE: Two-phase streaming for fast initial load === // === PHASE 1: Stream AppInfoMini as chunks complete (progressive loading) === Task.detached(priority: .userInitiated) { let chunks = createOptimalChunks(from: apps, minChunkSize: 10, maxChunkSize: 40) let queue = DispatchQueue(label: "com.pearcleaner.appinfo.mini", qos: .userInitiated, attributes: .concurrent) let group = DispatchGroup() var allMiniInfos: [AppInfoMini] = [] let resultsQueue = DispatchQueue(label: "com.pearcleaner.appinfo.mini.results") for chunk in chunks { group.enter() queue.async { autoreleasepool { let chunkMiniInfos: [AppInfoMini] = chunk.compactMap { appURL in autoreleasepool { let appPath = appURL.path // Use mini version for fast initial load if let appMetadata = metadataDictionary[appPath] { return MetadataAppInfoFetcher.getAppInfoMini(fromMetadata: appMetadata, atPath: appURL) } else { // Fallback to full version if no metadata return AppInfoFetcher.getAppInfo(atPath: appURL)?.toMini() } } } resultsQueue.sync { allMiniInfos.append(contentsOf: chunkMiniInfos) } // Stream to UI as each chunk completes let currentBatch = chunkMiniInfos.map { $0.toAppInfo() } Task { @MainActor in AppState.shared.sortedApps.append(contentsOf: currentBatch) // Sort after each addition to maintain alphabetical order AppState.shared.sortedApps.sort { $0.appName.sortKey < $1.appName.sortKey } } } group.leave() } } // Wait for all chunks to complete before launching Phase 2 group.notify(queue: DispatchQueue.global(qos: .utility)) { // === PHASE 2: Background upgrade to full AppInfo (expensive operations) === for mini in allMiniInfos { autoreleasepool { // Upgrade mini to full AppInfo with all expensive properties let fullAppInfo = MetadataAppInfoFetcher.upgradeToFullAppInfo(mini: mini) // Update sorted array on main thread using path as stable identifier Task { @MainActor in if let targetIndex = AppState.shared.sortedApps.firstIndex(where: { $0.path == mini.path }) { AppState.shared.sortedApps[targetIndex] = fullAppInfo } } } } // Notify that all apps are fully loaded with complete AppInfo (if setting enabled) Task { @MainActor in let loadOnStartup = UserDefaults.standard.object(forKey: "settings.updater.loadOnStartup") as? Bool ?? true if loadOnStartup { NotificationCenter.default.post(name: NSNotification.Name("AllAppsFullyLoaded"), object: nil) } } } } // Return empty array - results will stream in via AppState updates return [] } else { // === FULL MODE: Load complete AppInfo immediately (for Updater, post-uninstall, etc.) === let chunks = createOptimalChunks(from: apps, minChunkSize: 10, maxChunkSize: 40) let queue = DispatchQueue(label: "com.pearcleaner.appinfo.full", qos: .userInitiated, attributes: .concurrent) let group = DispatchGroup() var allFullInfos: [AppInfo] = [] let resultsQueue = DispatchQueue(label: "com.pearcleaner.appinfo.full.results") for chunk in chunks { group.enter() queue.async { autoreleasepool { let chunkFullInfos: [AppInfo] = chunk.compactMap { appURL in autoreleasepool { let appPath = appURL.path // Load full AppInfo with all expensive properties if let appMetadata = metadataDictionary[appPath], let mini = MetadataAppInfoFetcher.getAppInfoMini(fromMetadata: appMetadata, atPath: appURL) { return MetadataAppInfoFetcher.upgradeToFullAppInfo(mini: mini) } else { // Fallback to direct full version return AppInfoFetcher.getAppInfo(atPath: appURL) } } } resultsQueue.sync { allFullInfos.append(contentsOf: chunkFullInfos) } } group.leave() } } group.wait() // Notify that all apps are fully loaded (if setting enabled) Task { @MainActor in if loadUpdatesOnStartup { NotificationCenter.default.post(name: NSNotification.Name("AllAppsFullyLoaded"), object: nil) } } // Sort alphabetically and return return allFullInfos.sorted { $0.appName.sortKey < $1.appName.sortKey } } } // Get directory path for darwin cache and temp directories func darwinCT() -> (String, String) { let command = "echo $(getconf DARWIN_USER_CACHE_DIR) $(getconf DARWIN_USER_TEMP_DIR)" // let command = "echo $(realpath $(getconf DARWIN_USER_CACHE_DIR)) $(realpath $(getconf DARWIN_USER_TEMP_DIR))" let process = Process() process.launchPath = "/bin/bash" process.arguments = ["-c", command] let pipe = Pipe() process.standardOutput = pipe process.launch() let data = pipe.fileHandleForReading.readDataToEndOfFile() guard let output = String(data: data, encoding: .utf8)?.trimmingCharacters( in: .whitespacesAndNewlines) else { printOS("Could not get DARWIN_USER_CACHE_DIR or DARWIN_USER_TEMP_DIR") return ("", "") } let paths = output.split(separator: " ").map(String.init) guard paths.count >= 2 else { printOS("Could not parse DARWIN_USER_CACHE_DIR or DARWIN_USER_TEMP_DIR") return ("", "") } return ( paths[0].trimmingCharacters(in: .whitespaces), paths[1].trimmingCharacters(in: .whitespaces) ) } func listAppSupportDirectories() -> [String] { let fileManager = FileManager.default let home = fileManager.homeDirectoryForCurrentUser let appSupportLocation = home.appendingPathComponent("Library/Application Support").path let exclusions = Set([ "MobileSync", ".DS_Store", "Xcode", "SyncServices", "networkserviceproxy", "DiskImages", "CallHistoryTransactions", "App Store", "CloudDocs", "icdd", "iCloud", "Instruments", "AddressBook", "FaceTime", "AskPermission", "CallHistoryDB", ]) let exclusionRegex = try! NSRegularExpression(pattern: "\\bcom\\.apple\\b", options: []) do { let directoryContents = try fileManager.contentsOfDirectory(atPath: appSupportLocation) return directoryContents.compactMap { directoryName in let fullPath = appSupportLocation.appending("/\(directoryName)") var isDirectory: ObjCBool = false guard fileManager.fileExists(atPath: fullPath, isDirectory: &isDirectory), isDirectory.boolValue else { return nil } // Check for exclusions using regex and provided list let excludeByRegex = exclusionRegex.firstMatch( in: directoryName, options: [], range: NSRange(location: 0, length: directoryName.utf16.count)) != nil if exclusions.contains(directoryName) || excludeByRegex { return nil } return directoryName } } catch { printOS("Error listing AppSupport directories: \(error.localizedDescription)") return [] } } // Load app paths on launch func reversePreloader( allApps: [AppInfo], appState: AppState, locations: Locations, fsm: FolderSettingsManager, completion: @escaping () -> Void = {} ) { @AppStorage("settings.interface.animationEnabled") var animationEnabled: Bool = true updateOnMain { appState.leftoverProgress.0 = String(localized: "Finding orphaned files, please wait...") } ReversePathsSearcher(appState: appState, locations: locations, fsm: fsm, sortedApps: allApps) .reversePathsSearch { updateOnMain { // printOS("Reverse search processed successfully") appState.showProgress = false withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { appState.leftoverProgress.1 = 0.0 } appState.leftoverProgress.0 = String( localized: "Reverse search completed successfully") } completion() } } // Load item in Files view func showAppInFiles( appInfo: AppInfo, appState: AppState, locations: Locations) { @AppStorage("settings.interface.animationEnabled") var animationEnabled: Bool = true @AppStorage("settings.general.searchSensitivity") var globalSensitivityLevel: SearchSensitivityLevel = .strict updateOnMain { appState.showProgress = true appState.appInfo = .empty appState.selectedItems = [] // Initialize per-app sensitivity from global setting if not already set if appState.perAppSensitivity[appInfo.path.path] == nil { appState.perAppSensitivity[appInfo.path.path] = globalSensitivityLevel } // Initialize the path finder and execute its search. AppPathFinder(appInfo: appInfo, locations: locations, appState: appState, sensitivityOverride: appState.perAppSensitivity[appInfo.path.path]).findPaths() appState.appInfo = appInfo appState.currentView = .files } } // Move files to trash using Authorization Services so it asks for user password if needed func moveFilesToTrash(appState: AppState, at fileURLs: [URL]) -> Bool { let validFileURLs = filterValidFiles(fileURLs: fileURLs) // Filter invalid files // Check if there are any valid files to delete guard !validFileURLs.isEmpty else { printOS("No valid files to move to Trash.") return false } let result = FileManagerUndo.shared.deleteFiles(at: validFileURLs) return result } func moveFilesToTrashCLI(at fileURLs: [URL]) -> Bool { let validFileURLs = filterValidFiles(fileURLs: fileURLs) // Filter invalid files // Check if there are any valid files to delete guard !validFileURLs.isEmpty else { printOS("No valid files to move to Trash.") return false } let result = FileManagerUndo.shared.deleteFiles(at: validFileURLs, isCLI: true) return result } func filterValidFiles(fileURLs: [URL]) -> [URL] { let fileManager = FileManager.default return fileURLs.filter { url in // Check if file or folder exists guard fileManager.fileExists(atPath: url.path) else { printOS("Skipping \(url.path): File or folder does not exist.") return false } // Unlock the file or folder if it is locked if url.isFileLocked { do { try removeImmutableAttribute(from: url) printOS("Unlocked \(url.path).") } catch { printOS("Skipping \(url.path): Failed to unlock file or folder (\(error)).") return false } } return true } } extension URL { var isFileLocked: Bool { do { let fileAttributes = try FileManager.default.attributesOfItem(atPath: self.path) if let isLocked = fileAttributes[.immutable] as? Bool { return isLocked } } catch { printOS("Error checking lock status for \(self.path): \(error)") } return false } } func removeImmutableAttribute(from url: URL) throws { let attributes = [FileAttributeKey.immutable: false] try FileManager.default.setAttributes(attributes, ofItemAtPath: url.path) } // Undo trash action func undoTrash() -> Bool { // Check if an undo action is available if FileManagerUndo.shared.undoManager.canUndo { FileManagerUndo.shared.undoManager.undo() playTrashSound(undo: true) return true } else { printOS("Undo Trash Error: No undo action available.") return false } } // Reload apps list func reloadAppsList( appState: AppState, fsm: FolderSettingsManager, delay: Double = 0.0, completion: @escaping () -> Void = {} ) { updateOnBackground(after: delay) { // Clear array before reload updateOnMain { appState.sortedApps = [] } // Use non-streaming mode for reloads (needs full AppInfo immediately) let apps = getSortedApps(paths: fsm.folderPaths, useStreaming: false) // Update appState with sorted results updateOnMain { appState.sortedApps = apps completion() } } } // Process CLI // ======================================================================================================== func handleLaunchMode() { var arguments = CommandLine.arguments // Filter out arguments that break CLI commands on startup arguments = arguments.filter { !["-NSDocumentRevisionsDebugMode", "YES", "-AppleTextDirection", "NO"].contains($0) } if let langIndex = arguments.firstIndex(of: "-AppleLanguages"), langIndex + 1 < arguments.count { arguments.remove(at: langIndex) arguments.remove(at: langIndex) } let termType = ProcessInfo.processInfo.environment["TERM"] let isRunningInTerminal = termType != nil && termType != "dumb" // Check if any CLI command arguments are present (even if not in terminal) // This handles cases like SUDO_ASKPASS where the app is launched without a terminal let cliCommands = ["uninstall", "list", "search", "help", "ask-password"] let hasCLICommand = arguments.dropFirst().contains { arg in cliCommands.contains(arg) || arg.hasPrefix("--") } if isRunningInTerminal || hasCLICommand { let locations = Locations() let fsm = FolderSettingsManager() PearCLI.setupDependencies(locations: locations, fsm: fsm) do { // Drop the program name as to not interfere with argument parsing let args = Array(arguments.dropFirst()) var command = try PearCLI.parseAsRoot(args) // Run the command if no errors in parsing were caught try command.run() } catch { PearCLI.exit(withError: error) // Cli exit } } } // MARK: - Translation Pruning /// Information about a language translation in an app bundle struct LanguageInfo: Identifiable, Hashable { let id = UUID() let code: String // e.g., "en", "es", "en-GB" let displayName: String // Localized language name let isPreferred: Bool // Is in user's macOS preferred languages let fileCount: Int // Number of files in .lproj folder let lprojPaths: [URL] // All .lproj folder paths for this language } /// Find all available language translations in an app bundle /// - Parameter appBundlePath: Path to .app bundle /// - Returns: Array of LanguageInfo for all languages found (excludes Base.lproj) func findAvailableLanguages(in appBundlePath: String) async -> [LanguageInfo] { let fileManager = FileManager.default // Find all .lproj folders in Resources, PlugIns, and Frameworks let searchPaths = ["Contents/Resources", "Contents/PlugIns", "Contents/Frameworks"] var lprojPathsByLang: [String: [URL]] = [:] for searchPath in searchPaths { let fullSearchPath = URL(fileURLWithPath: appBundlePath).appendingPathComponent(searchPath) guard fileManager.fileExists(atPath: fullSearchPath.path), let enumerator = fileManager.enumerator(at: fullSearchPath, includingPropertiesForKeys: [.isDirectoryKey], options: [.skipsHiddenFiles]) else { continue } while let fileURL = enumerator.nextObject() as? URL { if fileURL.pathExtension == "lproj" { let langCode = fileURL.deletingPathExtension().lastPathComponent // Skip Base.lproj - never removable if langCode != "Base" { lprojPathsByLang[langCode, default: []].append(fileURL) } } } } guard !lprojPathsByLang.isEmpty else { return [] } // Get user's preferred language codes (e.g., ["en-US", "fr-FR"]) let preferredLanguages = Locale.preferredLanguages // Extract full language codes and base codes separately let preferredFullCodes = Set(preferredLanguages) // e.g., ["en-US", "fr-FR"] let preferredBaseCodes = Set(preferredLanguages.map { String($0.prefix(2)) }) // e.g., ["en", "fr"] // Check if user has region-specific preferences (e.g., "en-US" vs just "en") let hasRegionalPreferences = preferredLanguages.contains { $0.contains("-") } // Build LanguageInfo for each language var languages: [LanguageInfo] = [] for (langCode, paths) in lprojPathsByLang { // Get base language code (e.g., "en" from "en-GB") let baseLangCode = String(langCode.prefix(2)) // Check if this is a preferred language // Keep if: // 1. Exact match with user's preferred language (e.g., "en-US" matches "en-US") // 2. Base language with no region specifier (e.g., "en" when user has "en-US") // 3. User has base-only preference (e.g., user has "en", keep all "en*" variants) let isPreferred: Bool if preferredFullCodes.contains(langCode) { // Exact match (e.g., user has "en-US", language is "en-US") isPreferred = true } else if !langCode.contains("-") && preferredBaseCodes.contains(langCode) { // Base language without region (e.g., "en" when user has "en-US") isPreferred = true } else if !hasRegionalPreferences && preferredBaseCodes.contains(baseLangCode) { // User has base-only preference (e.g., user has "en", keep all "en*") isPreferred = true } else { isPreferred = false } // Count files in first .lproj folder (representative) let fileCount = (try? fileManager.contentsOfDirectory(atPath: paths[0].path))?.count ?? 0 // Get localized display name let displayName = Locale.current.localizedString(forLanguageCode: langCode) ?? langCode languages.append(LanguageInfo( code: langCode, displayName: displayName, isPreferred: isPreferred, fileCount: fileCount, lprojPaths: paths )) } // Sort: preferred first, then alphabetically return languages.sorted { first, second in if first.isPreferred != second.isPreferred { return first.isPreferred } return first.displayName.localizedCaseInsensitiveCompare(second.displayName) == .orderedAscending } } /// Remove translation files manually based on user selection /// - Parameter languagesToRemove: Array of LanguageInfo objects to remove func pruneLanguagesManual(languagesToRemove: [LanguageInfo]) async throws { guard !languagesToRemove.isEmpty else { return } // Flatten all lproj paths from all languages let lprojsToRemove = languagesToRemove.flatMap { $0.lprojPaths } // Get app name from AppState (always available since pruning is from selected app) let appName = AppState.shared.appInfo.appName // Use FileManagerUndo which automatically handles: // - Protected/system-owned files via privileged helper // - Bundled trash organization // - Undo support let bundleName = "\(appName) - Translations" let success = FileManagerUndo.shared.deleteFiles(at: lprojsToRemove, bundleName: bundleName) if !success { throw NSError( domain: "com.pearcleaner.prune", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to remove translation files."] ) } } // Remove translations that are not in use (AUTO mode - keeps user's macOS preferred languages) /// - Parameters: /// - appBundlePath: Path to .app bundle /// - showAlert: Whether to show success alert (default: false for silent background operation) func pruneLanguages(in appBundlePath: String, showAlert: Bool = false) async throws { // Use helper to get all languages with URLs and preferred status let allLanguages = await findAvailableLanguages(in: appBundlePath) guard !allLanguages.isEmpty else { return } // Filter to non-preferred languages (auto-prune keeps only preferred) var languagesToRemove = allLanguages.filter { !$0.isPreferred } // If no preferred languages found, keep English as fallback if allLanguages.allSatisfy({ !$0.isPreferred }) { // None are preferred - keep English if it exists, remove all others languagesToRemove = allLanguages.filter { language in !language.code.hasPrefix("en") } } // If we're removing everything, keep English as absolute fallback if languagesToRemove.count == allLanguages.count { if let english = allLanguages.first(where: { $0.code.hasPrefix("en") }) { languagesToRemove.removeAll { $0.code == english.code } } } // Delegate to manual prune (reuses same deletion logic) try await pruneLanguagesManual(languagesToRemove: languagesToRemove) // Show success alert if requested (for UI-triggered pruning) if showAlert { let removedCount = languagesToRemove.count let keptCount = allLanguages.count - removedCount await MainActor.run { showCustomAlert( title: "Translations Pruned", message: "Successfully removed \(removedCount) language\(removedCount == 1 ? "" : "s"). Kept \(keptCount) language\(keptCount == 1 ? "" : "s").", style: .informational ) } } } // FinderExtension Sequoia Fix func manageFinderPlugin(install: Bool) { let task = Process() task.launchPath = "/usr/bin/pluginkit" task.arguments = ["-e", "\(install ? "use" : "ignore")", "-i", "com.alienator88.Pearcleaner.FinderOpen"] task.launch() task.waitUntilExit() } // Brew cleanup //func getBrewCleanupCommand(for caskName: String) -> String { // let brewPath = isOSArm() ? "/opt/homebrew/bin/brew" : "/usr/local/bin/brew" // return "\(brewPath) uninstall --cask \(caskName) --zap --force && \(brewPath) cleanup && clear; echo '\nHomebrew cleanup was successful, you may close this window..\n'" //} // MARK: - Cask Lookup Cache /// Metadata extracted from Homebrew cask JSON files struct CaskMetadata { let caskName: String // from "full_token" field in cask JSON let autoUpdates: Bool? // from "auto_updates" field in cask JSON } private var caskLookupTable: [String: CaskMetadata]? // appName → CaskMetadata private let caskLookupQueue = DispatchQueue(label: "com.pearcleaner.cask.lookup", attributes: .concurrent) /// Get full cask metadata including auto_updates flag /// Returns CaskMetadata with cask name and auto_updates flag from Homebrew cask JSON /// - Parameters: /// - appName: The display name from kMDItemDisplayName (e.g., "Yandex Disk") /// - appPath: Optional app path URL to extract actual filename if display name doesn't match func getCaskInfo(for appName: String, appPath: URL? = nil, bundleId: String? = nil) -> CaskMetadata? { // First, try a read-only access let existingTable = caskLookupQueue.sync { return caskLookupTable } // If table doesn't exist, build it with a barrier write if existingTable == nil { caskLookupQueue.sync(flags: .barrier) { // Double-check inside the barrier to avoid duplicate work if caskLookupTable == nil { caskLookupTable = buildCaskLookupTable() } } } // Now safely read the result return caskLookupQueue.sync { // Try with display name first (from kMDItemDisplayName) if let result = caskLookupTable?[appName.lowercased()] { return result } // If not found, try with actual filename (handles localized names) // Example: Display name "Yandex Disk" won't match, but filename "Yandex.Disk.2" will if let appPath = appPath { let filename = appPath.lastPathComponent.replacingOccurrences(of: ".app", with: "").lowercased() if let result = caskLookupTable?[filename] { return result } } // Fallback: Try with bundle ID (for PKG-based casks like Google Drive) if let bundleId = bundleId, !bundleId.isEmpty { if let result = caskLookupTable?[bundleId.lowercased()] { return result } } return nil } } /// Invalidate cask lookup cache (call after installing/uninstalling casks) /// Next call to getCaskInfo will rebuild the table with updated cask metadata func invalidateCaskLookupCache() { caskLookupQueue.sync(flags: .barrier) { caskLookupTable = nil } } /// Get cask identifier (name) for an app /// Legacy function for backward compatibility - returns only cask name func getCaskIdentifier(for appName: String) -> String? { return getCaskInfo(for: appName)?.caskName } /// Build lookup table mapping app names to cask metadata /// Uses glob pattern to find all cask JSON files and extracts full_token, artifacts, and auto_updates private func buildCaskLookupTable() -> [String: CaskMetadata] { let caskroomPath = isOSArm() ? "/opt/homebrew/Caskroom/" : "/usr/local/Caskroom/" let fileManager = FileManager.default var appToCask: [String: CaskMetadata] = [:] // Safety check guard fileManager.fileExists(atPath: caskroomPath) else { printOS("Caskroom not found at: \(caskroomPath)") return [:] } // Single glob pattern to find ALL cask JSON files at once // Pattern: /opt/homebrew/Caskroom/*/.metadata/*/*/Casks/*.json let globPattern = "\(caskroomPath)*/.metadata/*/*/Casks/*.json" var globResult = glob_t() defer { globfree(&globResult) } guard glob(globPattern, 0, nil, &globResult) == 0 else { printOS("Glob pattern failed: \(globPattern)") return [:] } // Process each found JSON file let pathCount = Int(globResult.gl_pathc) for i in 0.. String? in // Extract app name from path like "/Applications/VeraCrypt.app/Contents" let components = path.components(separatedBy: "/") if let appIndex = components.firstIndex(where: { $0.hasSuffix(".app") }) { return components[appIndex] .replacingOccurrences(of: ".app", with: "") .lowercased() } return nil } // Add each discovered app to lookup table for appName in Set(appBundles) { // Use Set to avoid duplicates appToCask[appName] = CaskMetadata( caskName: caskName, autoUpdates: autoUpdates ) } } } } } } } } // Fallback for tap casks without .json files (only .rb files) // Use directory name as cask identifier let allCaskDirs = (try? fileManager.contentsOfDirectory(atPath: caskroomPath)) ?? [] for caskDirName in allCaskDirs where !caskDirName.hasPrefix(".") { let receiptPath = "\(caskroomPath)\(caskDirName)/.metadata/INSTALL_RECEIPT.json" guard let data = try? Data(contentsOf: URL(fileURLWithPath: receiptPath)), let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let artifacts = json["uninstall_artifacts"] as? [[String: Any]] else { continue } // Extract auto_updates flag let autoUpdates = json["auto_updates"] as? Bool // Extract app names from uninstall_artifacts for artifact in artifacts { if let apps = artifact["app"] as? [String] { for appStr in apps { let realAppName = appStr .replacingOccurrences(of: ".app", with: "") .lowercased() // Only add if not already in lookup table (JSON takes precedence) if appToCask[realAppName] == nil { // Use directory name as cask identifier appToCask[realAppName] = CaskMetadata( caskName: caskDirName, // e.g., "battery-toolkit" autoUpdates: autoUpdates ) } } } } } return appToCask } //func getCaskIdentifier(for appName: String) -> String? { // let caskroomPath = isOSArm() ? "/opt/homebrew/Caskroom/" : "/usr/local/Caskroom/" // let fileManager = FileManager.default // let lowercasedAppName = appName.lowercased() // // do { // // Get all cask directories from Caskroom, ignoring hidden files // let casks = try fileManager.contentsOfDirectory(atPath: caskroomPath).filter { // !$0.hasPrefix(".") // } // // for cask in casks { // // Construct the path to the cask directory // let caskSubPath = caskroomPath + cask // // // Get all version directories for this cask, ignoring hidden files // let versions = try fileManager.contentsOfDirectory(atPath: caskSubPath).filter { // !$0.hasPrefix(".") // } // // // Only check the first valid version directory to improve efficiency // if let latestVersion = versions.first { // let appDirectory = "\(caskSubPath)/\(latestVersion)/" // // // List all files in the version directory and check for .app file // // let appsInDir = try fileManager.contentsOfDirectory(atPath: appDirectory).filter { !$0.hasPrefix(".") } // let appsInDir = try fileManager.contentsOfDirectory(atPath: appDirectory).filter { // !$0.hasPrefix(".") && $0.hasSuffix(".app") // && !$0.lowercased().contains("uninstall") // } // if let appFile = appsInDir.first(where: { $0.hasSuffix(".app") }) { // let realAppName = appFile.replacingOccurrences(of: ".app", with: "") // .lowercased() // // Compare the lowercased app names for case-insensitive match // if realAppName == lowercasedAppName { // return realAppName.replacingOccurrences(of: " ", with: "-").lowercased() // } // } // } // } // } catch let error as NSError { // if !(error.domain == NSCocoaErrorDomain && error.code == 260) { // printOS("Cask Identifier: \(error)") // } // } // // // If no match is found, return nil // return nil //} // Print list of files locally func saveURLsToFile(appState: AppState, copy: Bool = false) { let urls = Set(appState.selectedItems) if copy { let homeDirectory = FileManager.default.homeDirectoryForCurrentUser.path var fileContent = "" let sortedUrls = urls.sorted { $0.path < $1.path } for url in sortedUrls { let pathWithTilde = url.path.replacingOccurrences(of: homeDirectory, with: "~") fileContent += "\(pathWithTilde)\n" } copyToClipboard(fileContent) } else { let panel = NSOpenPanel() panel.canChooseFiles = false panel.canChooseDirectories = true panel.allowsMultipleSelection = false panel.prompt = "Select Folder" if panel.runModal() == .OK, let selectedFolder = panel.url { let filePath = selectedFolder.appendingPathComponent( "Export-\(appState.appInfo.appName)(v\(appState.appInfo.appVersion)).txt") let homeDirectory = FileManager.default.homeDirectoryForCurrentUser.path var fileContent = "" let sortedUrls = urls.sorted { $0.path < $1.path } for url in sortedUrls { let pathWithTilde = url.path.replacingOccurrences(of: homeDirectory, with: "~") fileContent += "\(pathWithTilde)\n" } do { try fileContent.write(to: filePath, atomically: true, encoding: .utf8) printOS("File saved successfully at \(filePath.path)") // Open Finder and select the file NSWorkspace.shared.selectFile( filePath.path, inFileViewerRootedAtPath: filePath.deletingLastPathComponent().path) } catch { printOS("Error saving file: \(error)") } } else { printOS("Folder selection was canceled.") } } } /// Export debug information to a file with conditional content based on app context func exportDebugInfo(appState: AppState) { let panel = NSOpenPanel() panel.canChooseFiles = false panel.canChooseDirectories = true panel.allowsMultipleSelection = false panel.prompt = "Select Folder" if panel.runModal() == .OK, let selectedFolder = panel.url { // Determine filename based on context let filename: String if appState.currentView == .files && !appState.appInfo.bundleIdentifier.isEmpty { // App-specific debug filename = "Debug-\(appState.appInfo.appName)(v\(appState.appInfo.appVersion)).txt" } else { // System-only debug let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd_HH-mm-ss" let timestamp = dateFormatter.string(from: Date()) filename = "PearcleanerDiagnostics-\(timestamp).txt" } let filePath = selectedFolder.appendingPathComponent(filename) // Generate debug content based on context let debugContent: String if appState.currentView == .files && !appState.appInfo.bundleIdentifier.isEmpty { // Full debug: AppInfo + System debugContent = appState.appInfo.getDebugString() + "\n" + getSystemDebugString() } else { // System-only debug debugContent = getSystemDebugString() } do { try debugContent.write(to: filePath, atomically: true, encoding: .utf8) printOS("Debug info saved successfully at \(filePath.path)") // Open Finder and select the file NSWorkspace.shared.selectFile( filePath.path, inFileViewerRootedAtPath: filePath.deletingLastPathComponent().path) } catch { printOS("Error saving debug info: \(error)") } } else { printOS("Folder selection was canceled.") } } func exportUpdaterDebugInfo() { let panel = NSOpenPanel() panel.canChooseFiles = false panel.canChooseDirectories = true panel.allowsMultipleSelection = false panel.prompt = "Select Folder" if panel.runModal() == .OK, let selectedFolder = panel.url { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd_HH-mm-ss" let timestamp = dateFormatter.string(from: Date()) let filename = "UpdaterDebugLog-\(timestamp).txt" let filePath = selectedFolder.appendingPathComponent(filename) let systemInfo = getSystemDebugString() let updaterLogs = UpdaterDebugLogger.shared.generateDebugReport() let debugContent = systemInfo + "\n\n" + updaterLogs do { try debugContent.write(to: filePath, atomically: true, encoding: .utf8) printOS("Updater debug log saved successfully at \(filePath.path)") // Open Finder and select the file NSWorkspace.shared.selectFile( filePath.path, inFileViewerRootedAtPath: filePath.deletingLastPathComponent().path) // Clear logs after successful export UpdaterDebugLogger.shared.clearLogs() } catch { printOS("Error saving updater debug log: \(error)") } } else { printOS("Folder selection was canceled.") } } // Remove app from cache func removeApp(appState: AppState, withPath path: URL) async { @AppStorage("settings.general.brew") var brew: Bool = false await MainActor.run { // Remove from sortedApps if found if let index = appState.sortedApps.firstIndex(where: { $0.path == path }) { appState.sortedApps.remove(at: index) } } } // --- Remove bundle(s) from menubar items by patching the inner binary blob only --- //func removeBundles(_ bundleIDs: [String]) throws { // let plistPath = NSHomeDirectory() + "/Library/Group Containers/group.com.apple.controlcenter/Library/Preferences/group.com.apple.controlcenter.plist" // let plistURL = URL(fileURLWithPath: plistPath) // // // Load outer plist and inner blob // let outerData = try Data(contentsOf: plistURL) // var outerFormat: PropertyListSerialization.PropertyListFormat = .binary // guard var outerPlist = try PropertyListSerialization.propertyList(from: outerData, // options: .mutableContainersAndLeaves, // format: &outerFormat) as? [String: Any], // let trackedBlob = outerPlist["trackedApplications"] as? Data else { // throw NSError(domain: "PlistError", code: 1, // userInfo: [NSLocalizedDescriptionKey: "Invalid plist structure"]) // } // // // Decode trackedApplications // var innerFormat: PropertyListSerialization.PropertyListFormat = .binary // guard var innerList = try PropertyListSerialization.propertyList(from: trackedBlob, // options: .mutableContainersAndLeaves, // format: &innerFormat) as? [[String: Any]] else { // throw NSError(domain: "PlistError", code: 2, // userInfo: [NSLocalizedDescriptionKey: "Invalid inner plist structure"]) // } // // // Remove any entries with a matching bundle at any level // innerList.removeAll { entry in // // Top-level bundle // if let bundle = (entry["bundle"] as? [String: Any])?["_0"] as? String { // if bundleIDs.contains(where: { bundle.caseInsensitiveCompare($0) == .orderedSame }) { // return true // } // } // // Nested bundle under location -> menuItemLocations // if let location = entry["location"] as? [String: Any], // let menuItems = location["menuItemLocations"] as? [[String: Any]] { // for menuItem in menuItems { // if let bundle = (menuItem["bundle"] as? [String: Any])?["_0"] as? String { // if bundleIDs.contains(where: { bundle.caseInsensitiveCompare($0) == .orderedSame }) { // return true // } // } // } // } // return false // } // // // Re-encode and save // let newTrackedBlob = try PropertyListSerialization.data(fromPropertyList: innerList, // format: .binary, // options: 0) // outerPlist["trackedApplications"] = newTrackedBlob // let newOuterData = try PropertyListSerialization.data(fromPropertyList: outerPlist, // format: .binary, // options: 0) // try newOuterData.write(to: plistURL, options: .atomic) //} // --- Pearcleaner Uninstall --- func uninstallPearcleaner(appState: AppState, locations: Locations) { // Unload Sentinel Monitor if running launchctl(load: false) // Get app info for Pearcleaner let appInfo = AppInfoFetcher.getAppInfo(atPath: Bundle.main.bundleURL) // Find application files for Pearcleaner AppPathFinder( appInfo: appInfo!, locations: locations, appState: appState, completion: { // Kill Pearcleaner and tell Finder to trash the files let selectedItemsArray = Array(appState.selectedItems).filter { !$0.path.contains(".Trash") } let result = FileManagerUndo.shared.deleteFiles(at: selectedItemsArray) if result { playTrashSound() } exit(0) } ).findPaths() } // --- Load Plist file with SMAppService --- func launchctl(load: Bool, completion: @escaping () -> Void = {}) { let service = SMAppService.agent(plistName: "com.alienator88.PearcleanerSentinel.plist") if load { do { try service.register() } catch let error as NSError { printOS("Error registering PearcleanerSentinel: \(error)") } } else { do { try service.unregister() } catch let error as NSError { printOS("Error unregistering PearcleanerSentinel: \(error)") } } completion() } func createTarArchive(appState: AppState) { // Filter the array to include only paths under /Users/, /Applications/, or /Library/ let allowedPaths = Array(appState.selectedItems).filter { $0.path.starts(with: "/Users/") || $0.path.starts(with: "/Applications/") } guard !allowedPaths.isEmpty else { printOS("No valid paths provided.") return } // Create save panel let savePanel = NSSavePanel() // savePanel.allowedContentTypes = [.zip] savePanel.canCreateDirectories = true savePanel.showsTagField = false // Set default filename savePanel.nameFieldStringValue = "Bundle-\(appState.appInfo.appName).tar" savePanel.allowedContentTypes = [UTType(filenameExtension: "tar")!] // Show save panel let response = savePanel.runModal() guard response == .OK, let finalDestination = savePanel.url else { printOS("Archive export cancelled.") return } do { let tempDir = FileManager.default.temporaryDirectory.appendingPathComponent( "Bundle-" + appState.appInfo.appName) // Create a temporary directory to organize the paths try FileManager.default.createDirectory( at: tempDir, withIntermediateDirectories: true, attributes: nil) for path in allowedPaths { // Compute the relative path for each file let relativePath: String if path.path.starts(with: "/Users/") { relativePath = String(path.path.dropFirst("/Users/".count)) } else if path.path.starts(with: "/Applications/") { relativePath = "Applications/" + String(path.path.dropFirst("/Applications/".count)) } else { continue } // Create subdirectories as needed in the temporary directory let destinationPath = tempDir.appendingPathComponent(relativePath) try FileManager.default.createDirectory( at: destinationPath.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil) // Copy the file to the corresponding relative path in the temporary directory try FileManager.default.copyItem(at: path, to: destinationPath) } // Use `ditto` to create the tar archive let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/bin/ditto") process.arguments = [ "-c", "-k", "--sequesterRsrc", "--keepParent", tempDir.path, finalDestination.path, ] let pipe = Pipe() process.standardError = pipe try process.run() process.waitUntilExit() // Check for process errors if process.terminationStatus != 0 { let errorData = pipe.fileHandleForReading.readDataToEndOfFile() let errorMessage = String(data: errorData, encoding: .utf8) ?? "Unknown error" throw NSError( domain: "com.alienator88.Pearcleaner.archiveExport", code: Int(process.terminationStatus), userInfo: [NSLocalizedDescriptionKey: errorMessage]) } // Clean up the temporary directory try FileManager.default.removeItem(at: tempDir) printOS("Archive created successfully at \(finalDestination.path)") } catch { printOS("Error creating tar archive: \(error)") } } ================================================ FILE: Pearcleaner/Logic/PKG/PKBOM.h ================================================ // // Generated by class-dump 3.5 (64 bit) (Debug version compiled May 21 2020 17:01:39). // // Copyright (C) 1997-2019 Steve Nygard. // @class NSData; struct _BOMBom; struct _BOMFSObject; @interface PKBOM : NSObject { NSData *_bomData; struct _BOMBom *_BOMBom; } + (BOOL)_setAttributes:(id)arg1 ofBOMFSObject:(struct _BOMFSObject *)arg2; + (id)_attributesOfBOMFSObject:(struct _BOMFSObject *)arg1; + (id)_NSFileTypeFromBOMFSObjType:(int)arg1; - (unsigned long long)totalSize; - (unsigned long long)fileCount; - (id)subpathsOfDirectoryAtPath:(id)arg1; - (id)directoryEnumerator; - (id)attributesOfItemAtPath:(id)arg1; - (struct _BOMBom *)BOMBom; - (void)dealloc; - (id)initWithBOMData:(id)arg1; - (id)initWithBOMPath:(id)arg1; @end ================================================ FILE: Pearcleaner/Logic/PKG/PKBundleComponent.h ================================================ // // Generated by class-dump 3.5 (64 bit) (Debug version compiled May 21 2020 17:01:39). // // Copyright (C) 1997-2019 Steve Nygard. // #import "PKComponent.h" @class NSString, PKBundleComponentVersion; @interface PKBundleComponent : PKComponent { NSString *_storageType; NSString *_bundlePath; BOOL _onDisk; PKBundleComponentVersion *_bundleVersion; int _overwriteAction; } + (void)enumerateBundleComponentsUnderRoot:(id)arg1 usingBlock:(id)arg2; + (BOOL)_enumerateBundleComponentsUnderURL:(id)arg1 relativeToRoot:(id)arg2 parentComponent:(id)arg3 usingBlock:(id)arg4; @property int overwriteAction; // @synthesize overwriteAction=_overwriteAction; @property(readonly) NSString *bundleName; @property(readonly) NSString *bundlePath; @property(readonly) NSString *storageType; - (id)subpaths; - (id)prefixPath; - (long long)versionCompare:(id)arg1; @property(readonly) PKBundleComponentVersion *bundleVersion; - (void)dealloc; - (id)initWithBundleAtPath:(id)arg1 relativeToDestination:(id)arg2; - (id)initWithIdentifier:(id)arg1 versionAttributes:(id)arg2 bundlePath:(id)arg3 storageType:(id)arg4; - (id)initWithIdentifier:(id)arg1 versionAttributes:(id)arg2 bundlePath:(id)arg3; - (id)_bundle; @end ================================================ FILE: Pearcleaner/Logic/PKG/PKBundleComponentVersion.h ================================================ // // Generated by class-dump 3.5 (64 bit) (Debug version compiled May 21 2020 17:01:39). // // Copyright (C) 1997-2019 Steve Nygard. // #import @class NSDictionary, NSString; @interface PKBundleComponentVersion : NSObject { NSString *_shortVersionString; NSString *_origShortVersionString; NSString *_bundleVersion; NSString *_origBundleVersion; NSString *_sourceVersion; NSString *_buildVersion; BOOL _isOnDisk; } + (id)_orderedVersionKeys; + (id)_standardizedTupleVersionString:(id)arg1; + (id)_combinedVersionAndInfoDictionaryForCFBundle:(struct __CFBundle *)arg1; + (id)_combinedVersionAndInfoDictionaryForBundle:(id)arg1; + (id)bundleComponentVersionWithOnDiskCFBundle:(struct __CFBundle *)arg1; + (id)bundleComponentVersionWithOnDiskBundle:(id)arg1; + (id)bundleComponentVersionWithPackageAttributes:(id)arg1; + (BOOL)_checksBuildVersion; @property(readonly) NSString *combinedVersionString; @property(readonly) NSDictionary *attributeDictionary; - (BOOL)_isOnDiskComponent; @property(readonly) NSString *buildVersion; @property(readonly) NSString *sourceVersion; @property(readonly) NSString *bundleVersion; @property(readonly) NSString *shortVersionString; - (long long)compare:(id)arg1; - (id)description; - (void)dealloc; - (id)initWithAttributes:(id)arg1 forOnDiskComponent:(BOOL)arg2; - (id)initWithShortVersionString:(id)arg1 bundleVersion:(id)arg2 sourceVersion:(id)arg3 buildVersion:(id)arg4 forOnDiskComponent:(BOOL)arg5; @end ================================================ FILE: Pearcleaner/Logic/PKG/PKComponent.h ================================================ // // Generated by class-dump 3.5 (64 bit) (Debug version compiled May 21 2020 17:01:39). // // Copyright (C) 1997-2019 Steve Nygard. // #import @class NSArray, NSMutableArray, NSMutableDictionary, NSMutableSet, NSString, PKPackage; @interface PKComponent : NSObject { NSString *identifier; NSString *version; NSString *prefixPath; BOOL relocatable; BOOL versionChecked; BOOL strictIdentifier; NSArray *subpaths; PKPackage *package; NSString *destinationPath; NSMutableDictionary *_scriptsDictionary; NSMutableDictionary *_pathToPatchInfoDictionary; NSMutableDictionary *_pathToRequiredFilesDictionary; NSMutableArray *_deferredInstallPaths; NSMutableSet *_doNotObsoletePaths; NSMutableSet *_doNotObsoletePrefixes; } + (BOOL)isInstallablePath:(id)arg1; + (id)findComponentsWithIdentifier:(id)arg1 version:(id)arg2 destination:(id)arg3; + (id)findComponentsWithIdentifier:(id)arg1 destination:(id)arg2; + (id)_findComponentsViaSpotlightWithIdentifier:(id)arg1 version:(id)arg2 destination:(id)arg3; + (id)_findComponentViaLaunchServicesWithIdentifier:(id)arg1 version:(id)arg2 destination:(id)arg3; + (BOOL)_bundleAtPath:(id)arg1 matchesVersion:(id)arg2; + (BOOL)_componentPath:(id)arg1 matchesDestination:(id)arg2; @property(retain) NSArray *subpaths; // @synthesize subpaths; @property BOOL strictIdentifier; // @synthesize strictIdentifier; @property BOOL versionChecked; // @synthesize versionChecked; @property BOOL relocatable; // @synthesize relocatable; @property(retain) NSString *prefixPath; // @synthesize prefixPath; @property(retain) NSString *version; // @synthesize version; @property(retain) NSString *identifier; // @synthesize identifier; - (long long)versionCompare:(id)arg1; - (long long)compare:(id)arg1; - (BOOL)isEqual:(id)arg1; - (unsigned long long)hash; - (void)setScript:(id)arg1 forType:(id)arg2; - (id)scriptForType:(id)arg1; @property(retain) NSString *destinationPath; // @synthesize destinationPath; @property PKPackage *package; // @synthesize package; - (id)description; - (id)componentKey; - (void)dealloc; - (id)initWithIdentifier:(id)arg1 version:(id)arg2; - (id)_doNotObsoletePrefixes; - (id)_doNotObsoletePaths; - (id)_pathToRequiredFilesDictionary; - (id)_pathToPatchInfoDictionary; - (id)_deferredInstallPaths; - (id)_scriptsDictionary; @end ================================================ FILE: Pearcleaner/Logic/PKG/PKGManager.swift ================================================ // // PKGManager.swift // Pearcleaner // // Wrapper for Apple's private PackageKit framework APIs // import Foundation import AlinFoundation @available(macOS 10.5, *) class PKGManager { // MARK: - Package Enumeration /// Get all installed packages on a volume /// - Parameter volume: Volume path (default: "/") /// - Returns: Array of PKReceipt objects static func getAllPackages(volume: String = "/") -> [PKReceipt] { guard let receipts = PKReceipt.receiptsOnVolume(atPath: volume) as? [PKReceipt] else { return [] } return receipts } // MARK: - Package Information /// Extract package information from a PKReceipt /// - Parameter receipt: PKReceipt object /// - Returns: Structured PackageInfo object static func getPackageInfo(from receipt: PKReceipt) -> PackageInfo? { guard let packageId = receipt.packageIdentifier() as? String else { return nil } let packageName = "" let packageFileName = (receipt._packageName() as? String) ?? "" let version = (receipt.packageVersion() as? String) ?? "" let installDate = formatInstallDate(receipt.installDate()) let installLocation = (receipt.installPrefixPath() as? String) ?? "/" let installProcessName = (receipt.installProcessName() as? String) ?? "" // Get package groups (e.g., com.apple.group.documentation) let packageGroups = (receipt.packageGroups() as? [String]) ?? [] // Get additional info string if available let additionalInfo = (receipt.additionalInfo() as? String) ?? "" // Check if package is secure/signed let isSecure = receipt._isSecure() // Get all receipt storage paths let receiptStoragePaths = (receipt.receiptStoragePaths() as? [String]) ?? [] // Get receipt path (main plist file) let receiptPath = receiptStoragePaths.first(where: { $0.hasSuffix(".plist") }) ?? "/var/db/receipts/\(packageId).plist" // Get BOM info if available var totalSizeFromBOM: Int64 = 0 var totalFilesInBOM: Int = 0 if let bomInfo = getBOMInfo(for: receipt) { totalSizeFromBOM = bomInfo.totalSize totalFilesInBOM = bomInfo.fileCount } return PackageInfo( packageId: packageId, packageName: packageName, packageFileName: packageFileName, version: version, installDate: installDate, installProcessName: installProcessName, bomFiles: [], receiptPath: receiptPath, installLocation: installLocation, bomFilesLoaded: false, packageGroups: packageGroups, additionalInfo: additionalInfo, isSecure: isSecure, receiptStoragePaths: receiptStoragePaths, totalSizeFromBOM: totalSizeFromBOM, totalFilesInBOM: totalFilesInBOM ) } // MARK: - BOM Operations /// Get BOM statistics (size and file count) /// - Parameter receipt: PKReceipt object /// - Returns: Tuple with total size and file count, or nil if BOM not available static func getBOMInfo(for receipt: PKReceipt) -> (totalSize: Int64, fileCount: Int)? { guard let bomPath = findBOMPath(for: receipt) else { return nil } guard let bom = PKBOM(bomPath: bomPath) else { return nil } let totalSize = Int64(bom.totalSize()) let fileCount = Int(bom.fileCount()) return (totalSize, fileCount) } /// Get all files from package BOM /// - Parameters: /// - receipt: PKReceipt object /// - installLocation: Install prefix path /// - Returns: Array of absolute file paths static func getPackageFiles(receipt: PKReceipt, installLocation: String) -> [String] { guard let enumerator = receipt._directoryEnumerator() as? NSEnumerator else { return [] } var files: [String] = [] let prefixPath = installLocation.hasSuffix("/") ? installLocation : installLocation + "/" while let path = enumerator.nextObject() as? String { // Build absolute path let absolutePath: String if path.hasPrefix("/") { absolutePath = path } else { absolutePath = prefixPath + path } // Filter out Apple resource fork files if !absolutePath.contains("._") { files.append(absolutePath) } } return files } /// Find BOM file path for a receipt /// - Parameter receipt: PKReceipt object /// - Returns: BOM file path or nil if not found private static func findBOMPath(for receipt: PKReceipt) -> String? { guard let receiptPaths = receipt.receiptStoragePaths() as? [String] else { return nil } // Find .bom file in receipt storage paths return receiptPaths.first(where: { $0.hasSuffix(".bom") }) } // MARK: - Package Removal /// Get all receipt file paths that need to be deleted to forget a package /// - Parameter receipt: PKReceipt object /// - Returns: Array of file paths to delete static func getReceiptFilePaths(for receipt: PKReceipt) -> [String] { return (receipt.receiptStoragePaths() as? [String]) ?? [] } // MARK: - Helpers /// Format install date from PKReceipt /// - Parameter date: Date object from receipt /// - Returns: Formatted date string or Unix timestamp private static func formatInstallDate(_ date: Any?) -> String { if let date = date as? Date { return String(Int(date.timeIntervalSince1970)) } return "" } /// Check if a package is secure/signed /// - Parameter receipt: PKReceipt object /// - Returns: True if package is secure static func isPackageSecure(receipt: PKReceipt) -> Bool { return receipt._isSecure() } /// Get package groups /// - Parameter receipt: PKReceipt object /// - Returns: Array of group identifiers static func getPackageGroups(receipt: PKReceipt) -> [String] { return (receipt.packageGroups() as? [String]) ?? [] } } ================================================ FILE: Pearcleaner/Logic/PKG/PKInstallHistory.h ================================================ // // Generated by class-dump 3.5 (64 bit) (Debug version compiled May 21 2020 17:01:39). // // Copyright (C) 1997-2019 Steve Nygard. // @class NSString; @interface PKInstallHistory : NSObject { NSString *_path; } + (id)defaultHistory; + (id)historyOnVolume:(id)arg1; - (id)installedItems; - (BOOL)addInstallRequest:(id)arg1; - (void)dealloc; - (id)initWithPath:(id)arg1; @end ================================================ FILE: Pearcleaner/Logic/PKG/PKPackage.h ================================================ // // Generated by class-dump 3.5 (64 bit) (Debug version compiled May 21 2020 17:01:39). // // Copyright (C) 1997-2019 Steve Nygard. // @class NSArray, NSDictionary, NSString, NSURL, PKArchive, PKPackageInfo; @interface PKPackage : NSObject { NSURL *_url; NSDictionary *_options; PKArchive *_archive; PKPackageInfo *_packageInfo; NSDictionary *_componentMap; NSArray *_firmwareBundleComponents; BOOL _populatedSubpaths; } + (id)packageWithURL:(id)arg1 options:(id)arg2; + (id)packageWithURL:(id)arg1; + (id)packageWithPath:(id)arg1 options:(id)arg2; + (id)packageWithPath:(id)arg1; + (id)_allPackageClasses; + (BOOL)canInitWithURL:(id)arg1; - (void)enumerateBundleComponentsUsingBlock:(id)arg1; - (void)_populateComponentSubpaths; @property(readonly) NSURL *fileURL; @property(readonly) PKArchive *archive; - (BOOL)extractFilesForBundleComponent:(id)arg1 toPath:(id)arg2 error:(id *)arg3; - (id)firmwareComponents; - (id)subpathsForComponent:(id)arg1; - (id)componentForIdentifier:(id)arg1; - (id)components; - (id)_componentMap; - (id)directoryEnumerator; - (id)payloadExtractorWithDestination:(id)arg1 externalRoot:(id)arg2 error:(id *)arg3; - (id)scriptsExtractorWithDestination:(id)arg1 error:(id *)arg2; @property(readonly) NSString *scriptsSubpath; - (id)_scriptsDirectory; @property(readonly) PKPackageInfo *packageInfo; @property(readonly) NSString *packageVersion; @property(readonly) NSString *packageIdentifier; - (BOOL)isEqual:(id)arg1; - (unsigned long long)hash; - (id)description; - (void)dealloc; - (id)copyWithZone:(struct _NSZone *)arg1; - (void)encodeWithCoder:(id)arg1; - (id)initWithCoder:(id)arg1; - (id)_staticObsoleteFilesOrRecursiveDirectories; - (id)_staticObsoleteDirectories; - (id)_staticObsoleteFiles; - (id)BOMData; - (id)initWithURL:(id)arg1 options:(id)arg2; - (id)firmwareComponentsOfType:(long long)arg1; @end ================================================ FILE: Pearcleaner/Logic/PKG/PKPackageChecker.h ================================================ // // Generated by class-dump 3.5 (64 bit) (Debug version compiled May 21 2020 17:01:39). // // Copyright (C) 1997-2019 Steve Nygard. // #import @class NSArray, NSDictionary, NSMutableArray, NSString, PKPackageInfo; @interface PKPackageChecker : NSObject { NSString *_contentPath; PKPackageInfo *_packageInfo; NSDictionary *_componentMap; NSMutableArray *_checkResults; NSArray *_checkedAttrs; } + (id)stringForCheckErrorCode:(int)arg1; + (id)_defaultCheckedFileAttributes; @property(copy) NSArray *checkedFileAttributes; // @synthesize checkedFileAttributes=_checkedAttrs; - (BOOL)_isValidScriptAtPath:(id)arg1 error:(id *)arg2; - (id)_displayStringForBundleVersion:(id)arg1; - (void)_checkBundle:(id)arg1 againstVersion:(id)arg2 usingDisplayPath:(id)arg3; - (void)_searchForComponentsInDirectory:(id)arg1 addTo:(id)arg2; - (BOOL)_shouldValidatePayload; - (void)_checkScriptsAgainstPackageInfo:(id)arg1; - (void)_checkPayloadAgainstPackageInfo:(id)arg1; - (void)_checkPayloadAgainstBom; - (id)checkResults; - (void)dealloc; - (id)initWithUnarchivedPackage:(id)arg1 error:(id *)arg2; @end ================================================ FILE: Pearcleaner/Logic/PKG/PKPackageInfo.h ================================================ // // Generated by class-dump 3.5 (64 bit) (Debug version compiled May 21 2020 17:01:39). // // Copyright (C) 1997-2019 Steve Nygard. // #import @class NSXMLDocument; @interface PKPackageInfo : NSObject { NSXMLDocument *_XMLDocument; } + (id)_bundleIdentifierFromBundleElement:(id)arg1; - (void)setValue:(id)arg1 forKey:(id)arg2; - (id)valueForKey:(id)arg1; @property(readonly) NSXMLDocument *XMLDocument; - (void)dealloc; - (id)initWithIdentifier:(id)arg1; - (id)initWithIdentifier:(id)arg1 version:(id)arg2; - (id)initWithData:(id)arg1; - (id)_initWithXMLDocument:(id)arg1; - (void)_setDontObsoletePathsWithSet:(id)arg1; - (void)_setFileElementTurdsWithComponents:(id)arg1; - (void)_setComponentScriptsWithComponents:(id)arg1; - (void)_setBundleIdentifiersAsRelocatable:(id)arg1; - (void)_setBundleIdentifiersForStrict:(id)arg1; - (void)_setBundleIdentifiersForAtomicUpdate:(id)arg1; - (void)_setBundleIdentifiersForUpdate:(id)arg1; - (void)_setBundleIdentifiersForUpgrade:(id)arg1; - (void)_setBundleIdentifiersForVersionChecking:(id)arg1; - (void)__setBundleIdentifiers:(id)arg1 forDirective:(id)arg2; - (void)_setBundleDefinitionsWithBundleAttributeDictionaries:(id)arg1; - (void)setGroups:(id)arg1; - (id)groups; - (void)setstaticObsoleteFileOrRecursiveDirectoryListPath:(id)arg1; - (id)staticObsoleteFileOrRecursiveDirectoryListPath; - (void)setStaticObsoleteDirectoryListPath:(id)arg1; - (id)staticObsoleteDirectoryListPath; - (void)setStaticObsoleteFileListPath:(id)arg1; - (id)staticObsoleteFileListPath; - (void)setPostInstallScriptPath:(id)arg1; - (void)setPostinstallScriptPath:(id)arg1; - (id)postInstallScriptPath; - (id)postinstallScriptPath; - (void)setPreInstallScriptPath:(id)arg1; - (void)setPreinstallScriptPath:(id)arg1; - (id)preInstallScriptPath; - (id)preinstallScriptPath; - (id)_lastElementForScriptName:(id)arg1 componentIdentifier:(id)arg2 createIfNeeded:(BOOL)arg3; - (void)setUpdatePayloadSize:(unsigned long long)arg1; - (unsigned long long)updatePayloadSize; - (void)setPayloadFileCount:(unsigned long long)arg1; - (unsigned long long)payloadFileCount; - (void)setPayloadInstallSize:(unsigned long long)arg1; - (unsigned long long)payloadInstallSize; - (void)setPayloadExternalRoot:(id)arg1; - (id)payloadExternalRoot; - (void)setUseHFSPlusCompression:(id)arg1; - (id)useHFSPlusCompression; - (void)setMinimumSystemVersion:(id)arg1; - (id)minimumSystemVersion; - (void)setGeneratorVersion:(id)arg1; - (id)generatorVersion; - (void)setRequiresDistributionCheck:(BOOL)arg1; - (BOOL)requiresDistributionCheck; - (void)setUpdatePackage:(BOOL)arg1; - (BOOL)updatePackage; - (void)setSystemVolumeGroupInstallLocation:(id)arg1; - (id)systemVolumeGroupInstallLocation; - (void)setInstallLocation:(id)arg1; - (id)installLocation; - (void)setRestartAction:(int)arg1; - (int)restartAction; - (void)setAuthLevel:(int)arg1; - (int)authLevel; - (void)setVersion:(id)arg1; - (id)version; - (void)setIdentifier:(id)arg1; - (id)identifier; - (void)setPreserveACLs:(BOOL)arg1; - (BOOL)preserveACLs; - (void)setPreserveExtAttrs:(BOOL)arg1; - (BOOL)preserveExtAttrs; - (void)setContentType:(id)arg1; - (id)contentType; - (void)setShouldVerifyArchiveExplicitly:(BOOL)arg1; - (BOOL)shouldVerifyArchiveExplicitly; - (void)setScriptsInvalidateReceipt:(BOOL)arg1; - (BOOL)scriptsInvalidateReceipt; - (void)setAllowCustomInstallLocation:(BOOL)arg1; - (BOOL)allowCustomInstallLocation; - (void)setOverwritePermissions:(BOOL)arg1; - (BOOL)overwritePermissions; - (id)_dontObsoleteXMLElement; - (id)_deferredInstallXMLElement; - (id)_requiredFilesXMLElement; - (id)_patchXMLElement; - (id)_firmwareBundleXMLElements; - (id)_relocateBundleXMLElements; - (id)_strictIdentifierBundleXMLElements; - (id)_bundleAtomicUpdateBundleXMLElements; - (id)_bundleUpdateBundleXMLElements; - (id)_bundleUpgradeBundleXMLElements; - (id)_bundleVersionBundleXMLElements; - (id)_topLevelBundleXMLElements; - (id)_rootElement; - (void)_setDeferredInstallScriptPath:(id)arg1; - (id)_deferredInstallScriptPath; - (id)_firmwareBundlesWithComponentMap:(id)arg1; - (id)_identifierToComponentMapWithPackage:(id)arg1; - (void)_parseScriptsElementWithComponentMap:(id)arg1; - (void)_parseFileContainerElement:(id)arg1 container:(id)arg2 componentMap:(id)arg3 pathToComponentMap:(id)arg4; - (id)__bundlePathToExistingComponentMap:(id)arg1; - (void)__parseFileElement:(id)arg1 container:(id)arg2 component:(id)arg3; - (void)_parseRelocateElementsWithComponentMap:(id)arg1; - (void)_parseBundleDirectiveElementsWithComponentMap:(id)arg1; - (void)_parseBundleElements:(id)arg1 intoComponentMap:(id)arg2 withPackage:(id)arg3; - (id)_coalescedBundleElements; - (id)_coalescedBundleElementsFromElements:(id)arg1 withParentElement:(id)arg2 bundleIdentifierToBundleElementMap:(id)arg3; - (id)_generatedIdentifierForBundlePath:(id)arg1; - (id)_looseComponentIdentifier; - (id)_bundlePathFromBundleElement:(id)arg1; - (id)componentForIdentifier:(id)arg1; @end ================================================ FILE: Pearcleaner/Logic/PKG/PKProductInfo.h ================================================ // // Generated by class-dump 3.5 (64 bit) (Debug version compiled May 21 2020 17:01:39). // // Copyright (C) 1997-2019 Steve Nygard. // #import @class NSArray, NSDictionary, NSString, NSURL; @interface PKProductInfo : NSObject { NSDictionary *_productInfoDictionary; NSURL *_baseURL; NSArray *_cachedPackageReferences; } + (BOOL)supportsSecureCoding; - (id)copyWithZone:(struct _NSZone *)arg1; - (void)encodeWithCoder:(id)arg1; - (id)initWithCoder:(id)arg1; @property(readonly) NSURL *baseURL; @property(readonly) NSDictionary *dictionaryRepresentation; @property(readonly) NSArray *packageReferences; - (id)preferredDistributionURL; - (id)distributionURLForLocalization:(id)arg1; @property(readonly) NSArray *localizations; - (id)_distributionsDictionary; @property(readonly) NSString *productVersion; @property(readonly) NSString *productIdentifier; - (void)dealloc; - (id)initWithDictionary:(id)arg1 baseURL:(id)arg2; - (void)setProductIdentifier:(id)arg1 version:(id)arg2; - (void)setPackageReferences:(id)arg1; - (void)setDistributionURL:(id)arg1 forLocalization:(id)arg2; - (id)init; @end ================================================ FILE: Pearcleaner/Logic/PKG/PKReceipt.h ================================================ // // Generated by class-dump 3.5 (64 bit) (Debug version compiled May 21 2020 17:01:39). // // Copyright (C) 1997-2019 Steve Nygard. // @class NSMutableDictionary, NSString; @interface PKReceipt : NSObject { NSMutableDictionary *_receiptDictionary; NSString *_bomPath; void *_cachedBOM; NSString *_bundlePath; BOOL _isSecure; } + (void)_clearCache; + (id)receiptsAtPath:(id)arg1; + (id)receiptWithPackageFileName:(id)arg1 volume:(id)arg2; + (id)receiptWithIdentifier:(id)arg1 volume:(id)arg2; + (id)receiptsOnVolumeAtPath:(id)arg1 matchingPackageIdentifier:(id)arg2; + (id)receiptsOnVolumeAtPath:(id)arg1; + (id)_sortedReceiptsByPackageVersion:(id)arg1; + (id)_findReceiptsOnVolumeAtPath:(id)arg1; + (id)__findBundleReceiptsFromDirectory:(id)arg1; + (id)__findReceiptsFromBOMsDirectory:(id)arg1; + (id)_receiptDictionaryPathFromBOMPath:(id)arg1; + (id)_searchDirectoriesForBundleReceiptsOnDestination:(id)arg1; + (id)_searchDirectoriesForBOMReceiptsOnDestination:(id)arg1 returningSecureIndexes:(id *)arg2; + (id)_receiptsDirectoryPathForSandboxRoot:(id)arg1 destination:(id)arg2; + (id)_receiptsDirectoryPathForDestination:(id)arg1; + (id)_systemDataContentReceiptsDirectoryPathForDestination:(id)arg1; + (id)_systemContentReceiptsDirectoryPathForDestination:(id)arg1; + (id)_systemDataContentReceiptsDirectoryPathForSandboxRoot:(id)arg1 destination:(id)arg2; + (id)_systemContentReceiptsDirectoryPathForSandboxRoot:(id)arg1 destination:(id)arg2; + (void)_clearCacheWithNotification:(id)arg1; + (void)_clearCacheInOtherProcesses; + (void)_clearCacheLocally; + (id)_sharedReceiptsCache; - (id)additionalInfo; - (id)installPrefixPath; - (id)installProcessName; - (id)installDate; - (id)packageVersion; - (id)packageIdentifier; - (id)description; - (id)initWithBundlePkgPath:(id)arg1; - (id)initWithBOMPath:(id)arg1; - (void)dealloc; - (id)init; - (void)_setSecure:(BOOL)arg1; - (BOOL)_isSecure; - (BOOL)_updateACLsOfItemAtPath:(id)arg1 withFile:(id)arg2; - (BOOL)_updateSHA1ChecksumOfItemAtPath:(id)arg1 withFile:(id)arg2; - (void)_setACLString:(id)arg1 forItemAtPath:(id)arg2; - (id)_ACLStringOfItemAtPath:(id)arg1; - (void)_setSHA1ChecksumData:(id)arg1 forItemAtPath:(id)arg2; - (id)_SHA1ChecksumDataOfItemAtPath:(id)arg1; - (id)_attributesOfItemAtPath:(id)arg1; - (id)_directoryEnumerator; - (void)_freeBOM; - (id)_BOM; - (id)receiptStoragePaths; - (void)_setInstallPrefixPath:(id)arg1; - (void)_setInstallProcessName:(id)arg1; - (void)_setPackageName:(id)arg1; - (id)_packageName; - (void)_setPackageGroups:(id)arg1; - (id)packageGroups; - (BOOL)_removeReceiptInDirectory:(id)arg1 error:(id *)arg2; @end ================================================ FILE: Pearcleaner/Logic/PKG/Pearcleaner-Bridging-Header.h ================================================ // // Pearcleaner-Bridging-Header.h // Pearcleaner // // Use this file to import Objective-C headers for Swift. // #import // CFBundle Private API #import "CFBundle_Private.h" // PackageKit Private Framework Headers #import "PKBOM.h" #import "PKBundleComponent.h" #import "PKBundleComponentVersion.h" #import "PKComponent.h" #import "PKInstallHistory.h" #import "PKPackage.h" #import "PKPackageChecker.h" #import "PKPackageInfo.h" #import "PKProductInfo.h" #import "PKReceipt.h" ================================================ FILE: Pearcleaner/Logic/PasswordRequestHandler.swift ================================================ // // PasswordRequestHandler.swift // Pearcleaner // // Handles password requests from CLI via distributed notifications // Created by Alin Lupascu on 11/9/24. // import Foundation import AppKit import AlinFoundation class PasswordRequestHandler { static let shared = PasswordRequestHandler() private init() { setupObserver() } private func setupObserver() { DistributedNotificationCenter.default().addObserver( self, selector: #selector(handlePasswordRequest(_:)), name: NSNotification.Name("com.alienator88.Pearcleaner.passwordRequest"), object: nil ) } @objc private func handlePasswordRequest(_ notification: Notification) { guard let userInfo = notification.userInfo, let requestId = userInfo["requestId"] as? String, let message = userInfo["message"] as? String else { return } // Show password dialog on main thread DispatchQueue.main.async { let password = self.showPasswordDialog(message: message) // Send response back DistributedNotificationCenter.default().postNotificationName( NSNotification.Name("com.alienator88.Pearcleaner.passwordResponse"), object: nil, userInfo: [ "requestId": requestId, "password": password ?? "" ], deliverImmediately: true ) } } private func showPasswordDialog(message: String) -> String? { let alert = NSAlert() alert.messageText = "Pearcleaner" alert.informativeText = message alert.alertStyle = .informational alert.addButton(withTitle: "OK") alert.addButton(withTitle: "Cancel") let secureTextField = NSSecureTextField(frame: NSRect(x: 0, y: 0, width: 300, height: 24)) secureTextField.placeholderString = "Password" alert.accessoryView = secureTextField alert.window.initialFirstResponder = secureTextField NSApp.activate(ignoringOtherApps: true) let response = alert.runModal() return response == .alertFirstButtonReturn ? secureTextField.stringValue : nil } deinit { DistributedNotificationCenter.default().removeObserver(self) } } ================================================ FILE: Pearcleaner/Logic/ProcessEnv.swift ================================================ // // ProcessEnv.swift // Pearcleaner // // Created by Alin Lupascu on 10/6/25. // import Foundation public extension Process { static func executeAsUser(_ parameters: Process.ExecutionParameters) throws -> Data? { let userParams = parameters.userShellInvocation() let task = Process() task.executableURL = URL(fileURLWithPath: userParams.path) task.arguments = userParams.arguments task.environment = userParams.environment task.currentDirectoryURL = userParams.currentDirectoryURL return try? task.runAndReadStdout() } func runAndReadStdout() throws -> Data? { let pipe = Pipe() standardOutput = pipe try run() waitUntilExit() return try pipe.fileHandleForReading.readToEnd() } // MARK: - Execution Parameters /// Wraps up all of the parameters needed for starting a Process into one single type. struct ExecutionParameters: Codable, Hashable, Sendable { public var path: String public var arguments: [String] public var environment: [String : String]? public var currentDirectoryURL: URL? public init(path: String, arguments: [String] = [], environment: [String : String]? = nil, currentDirectoryURL: URL? = nil) { self.path = path self.arguments = arguments self.environment = environment self.currentDirectoryURL = currentDirectoryURL } public var command: String { return ([path] + arguments).joined(separator: " ") } /// Returns parameters that emulate an invocation in the user's shell /// /// This is done by executing: /// /// shellExecutablePath -ilc /// /// This method executes this with the `environment` environment /// variables set. But, it also ensures that the `TERM`, `HOME`, and /// `PATH` variables have values, if aren't present in `environment`. /// /// The `-i` and `-l` flags are critical, as they control how many /// shells read configuration files. public func userShellInvocation() -> ExecutionParameters { let processInfo = ProcessInfo.processInfo let shellPath = processInfo.shellExecutablePath let args = ["-ilc", command] let cwdURL = currentDirectoryURL let defaultEnv = ["TERM": "xterm-256color", "HOME": processInfo.homePath, "PATH": processInfo.path] let baseEnv = environment ?? defaultEnv let env = baseEnv.merging(defaultEnv, uniquingKeysWith: { (a, _) in a }) return ExecutionParameters(path: shellPath, arguments: args, environment: env, currentDirectoryURL: cwdURL) } } } extension ProcessInfo { /// The path to the current user's shell executable /// /// This attempts to query the `SHELL` environment variable, the /// password directory (via `getpwuid`), or if those fail /// falls back to "/bin/bash". public var shellExecutablePath: String { if let value = environment["SHELL"], !value.isEmpty { return value } if let passwd = getpwuid(getuid()), let cString = passwd.pointee.pw_shell { let shellPath = String(cString: cString) if !shellPath.isEmpty { return shellPath } } // this is a terrible fallback, but we need something return "/bin/bash" } /// Returns the value of PATH /// /// If PATH is set in the envrionment, it is returned. If not, /// the fallback value of "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" /// is returned. public var path: String { return environment["PATH"] ?? "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" } public var homePath: String { if let path = environment["HOME"] { return path } if let passwd = getpwuid(getuid()), let cString = passwd.pointee.pw_dir { return String(cString: cString) } return "/Users/\(userName)" } /// Capture the interactive-login shell environment /// /// This method attempts to reconstruct the user /// environment that would be set up when logging into /// a terminal session. public var userEnvironment: [String : String] { guard let data = try? Process.executeAsUser(.init(path: "/usr/bin/env", environment: environment)) else { return environment } return parseEnvOutput(data) } func parseEnvOutput(_ data: Data) -> [String : String] { guard let string = String(data: data, encoding: .utf8) else { return [:] } var env: [String: String] = [:] string.enumerateLines { (line, _) in guard let separatorIndex = line.firstIndex(of: "=") else { return } let key = line[.. Void = {}) { Task(priority: .high) { await GlobalConsoleManager.shared.appendOutput("Scanning \(locations.reverse.paths.count) system locations for orphaned files...\n", source: CurrentPage.orphans.title) if streamingMode { await self.processLocationsStreaming() } else { self.processLocations() self.calculateFileDetails() self.updateAppState() } completion() } } private func processLocationsStreaming() async { var batch: [(url: URL, size: Int64, icon: NSImage?)] = [] let batchSize = 10 var totalFound = 0 for location in locations.reverse.paths where fileManager.fileExists(atPath: location) { if shouldStop { break } let beforeCount = batch.count await processLocationStreaming(location, batch: &batch, batchSize: batchSize) let foundInLocation = batch.count - beforeCount if foundInLocation > 0 { totalFound += foundInLocation } } // Flush any remaining items if !batch.isEmpty { await flushBatch(&batch) } // Capture totalFound value before MainActor.run to avoid concurrency issues let finalCount = totalFound // Mark scanning as complete await MainActor.run { self.appState?.showProgress = false if !shouldStop { GlobalConsoleManager.shared.appendOutput("✓ Orphan scan complete, found \(finalCount) orphaned files\n", source: CurrentPage.orphans.title) } } } private func processLocationStreaming(_ location: String, batch: inout [(url: URL, size: Int64, icon: NSImage?)], batchSize: Int) async { do { let contents = try fileManager.contentsOfDirectory(atPath: location) for scannedItemName in contents { if shouldStop { return } let scannedItemURL = URL(fileURLWithPath: location).appendingPathComponent(scannedItemName) await processItemStreaming(scannedItemName, scannedItemURL: scannedItemURL, batch: &batch, batchSize: batchSize) } } catch { printOS("Error processing location: \(location), error: \(error)") } } private func processItemStreaming(_ scannedItemName: String, scannedItemURL: URL, batch: inout [(url: URL, size: Int64, icon: NSImage?)], batchSize: Int) async { let normalizedItemPath = scannedItemURL.standardizedFileURL.path.pearFormat() if formattedExclusionList.contains(normalizedItemPath) || normalizedItemPath.contains("dsstore") || normalizedItemPath.contains("daemonnameoridentifierhere") || formattedExclusionList.first(where: { normalizedItemPath.contains($0) }) != nil { return } let normalizedItemName = scannedItemName.pearFormat() guard !isUUIDFormatted(normalizedItemName), !skipReverse.contains(where: { normalizedItemName.contains($0) }), isSupportedFileType(at: scannedItemURL.path), !isRelatedToInstalledApp(scannedItemURL: scannedItemURL), !isExcludedByConditions(normalizedItemPath: normalizedItemPath) else { return } // Calculate file details immediately let size = totalSizeOnDisk(for: scannedItemURL) let icon = getIconForFileOrFolderNS(atPath: scannedItemURL) // Add to batch batch.append((url: scannedItemURL, size: size, icon: icon)) // Flush batch when it reaches the batch size if batch.count >= batchSize { await flushBatch(&batch) } } private func flushBatch(_ batch: inout [(url: URL, size: Int64, icon: NSImage?)]) async { let batchCopy = batch batch.removeAll() await MainActor.run { var updatedZombieFile = self.appState?.zombieFile ?? ZombieFile.empty for item in batchCopy { updatedZombieFile.fileSize[item.url] = item.size updatedZombieFile.fileIcon[item.url] = item.icon } // Log progress periodically (every 50 files) let currentCount = updatedZombieFile.fileSize.count if currentCount % 50 == 0 && currentCount > 0 { GlobalConsoleManager.shared.appendOutput("Scanned \(currentCount) orphaned files so far...\n", source: CurrentPage.orphans.title) } self.appState?.zombieFile = updatedZombieFile } } func reversePathsSearchCLI() -> [URL] { self.processLocationsCLI() return collection } private func processLocations() { for location in locations.reverse.paths where fileManager.fileExists(atPath: location) { dispatchGroup.enter() processLocation(location) dispatchGroup.leave() } } private func processLocationsCLI() { for location in locations.reverse.paths where fileManager.fileExists(atPath: location) { processLocation(location) } } private func processLocation(_ location: String) { do { let contents = try fileManager.contentsOfDirectory(atPath: location) contents.forEach { scannedItemName in let scannedItemURL = URL(fileURLWithPath: location).appendingPathComponent(scannedItemName) processItem(scannedItemName, scannedItemURL: scannedItemURL) } } catch { printOS("Error processing location: \(location), error: \(error)") } } private func processItem(_ scannedItemName: String, scannedItemURL: URL) { let normalizedItemPath = scannedItemURL.standardizedFileURL.path.pearFormat() if formattedExclusionList.contains(normalizedItemPath) || normalizedItemPath.contains("dsstore") || normalizedItemPath.contains("daemonnameoridentifierhere") || formattedExclusionList.first(where: { normalizedItemPath.contains($0) }) != nil { return } let normalizedItemName = scannedItemName.pearFormat() guard !isUUIDFormatted(normalizedItemName), !skipReverse.contains(where: { normalizedItemName.contains($0) }), isSupportedFileType(at: scannedItemURL.path), !isRelatedToInstalledApp(scannedItemURL: scannedItemURL), !isExcludedByConditions(normalizedItemPath: normalizedItemPath) else { return } collection.append(scannedItemURL) } private func isRelatedToInstalledApp(scannedItemURL: URL) -> Bool { let normalizedItemPath = scannedItemURL.path.pearFormat() for (_, cached) in cachedAppIdentifiers.enumerated() { // Only match if bundle ID or app name is at least 5 characters (avoids false positives from short words like "test", "alin", etc.) if !cached.formattedBundleId.isEmpty && cached.formattedBundleId.count >= 5 && normalizedItemPath.contains(cached.formattedBundleId) { return true } if !cached.formattedAppName.isEmpty && cached.formattedAppName.count >= 5 && normalizedItemPath.contains(cached.formattedAppName) { return true } // Check entitlements-based matching (using pre-formatted entitlements) for entitlementFormatted in cached.formattedEntitlements { if !entitlementFormatted.isEmpty && entitlementFormatted.count >= 5 && normalizedItemPath.contains(entitlementFormatted) { return true } } // Check if the path contains /Containers or /Group Containers if scannedItemURL.path.contains("/Containers/") { let containerName = scannedItemURL.containerNameByUUID().pearFormat() if !cached.formattedBundleId.isEmpty && cached.formattedBundleId.count >= 5 && containerName.contains(cached.formattedBundleId) { return true } } } return false } private func isExcludedByConditions(normalizedItemPath: String) -> Bool { for condition in conditions { // Ensure the condition's bundle_id matches an installed app (condition.bundle_id is already formatted in Conditions.swift) guard cachedAppIdentifiers.contains(where: { $0.formattedBundleId == condition.bundle_id || $0.formattedBundleId.contains(condition.bundle_id) }) else { continue } // Include keywords (condition.include is already formatted in Conditions.swift) if condition.include.contains(where: { normalizedItemPath.contains($0) }) { return true } // Include force if let includeForce = condition.includeForce, includeForce.contains(where: { normalizedItemPath.contains($0.path.pearFormat()) }) { return true } } return false } private func isUUIDFormatted(_ fileName: String) -> Bool { let uuidRegex = "^[0-9a-fA-F]{32}$" // UUID without dashes let regex = try? NSRegularExpression(pattern: uuidRegex) let range = NSRange(location: 0, length: fileName.utf16.count) return regex?.firstMatch(in: fileName, options: [], range: range) != nil } private func calculateFileDetails() { collection.forEach { path in let size = totalSizeOnDisk(for: path) fileSize[path] = size fileIcon[path] = getIconForFileOrFolderNS(atPath: path) } } private func updateAppState() { dispatchGroup.notify(queue: .main) { var updatedZombieFile = ZombieFile.empty updatedZombieFile.fileSize = self.fileSize updatedZombieFile.fileIcon = self.fileIcon let totalSize = self.fileSize.values.reduce(0, +) let sizeString = ByteCountFormatter.string(fromByteCount: totalSize, countStyle: .file) Task { @MainActor in GlobalConsoleManager.shared.appendOutput("✓ Found \(self.collection.count) orphaned files (\(sizeString))\n", source: CurrentPage.orphans.title) } self.appState?.zombieFile = updatedZombieFile self.appState?.showProgress = false } } } ================================================ FILE: Pearcleaner/Logic/TCC/TCCModels.swift ================================================ // // TCCModels.swift // Pearcleaner // // TCC (Transparency, Consent, and Control) data models for privacy permissions // import Foundation import SwiftUI // MARK: - TCC Permission /// Represents a single TCC permission entry from the database struct TCCPermission: Identifiable, Equatable { let id = UUID() let service: String let authValue: Int let authReason: Int? let lastModified: Date? let source: PermissionSource enum PermissionSource: String { case user = "USER" case system = "SYSTEM" } var displayName: String { TCCServiceMapper.friendlyName(for: service) } var sourceColor: Color { switch source { case .user: return .blue case .system: return .purple } } var statusText: String { switch authValue { case 0: return "Denied" case 1: return "Allowed" case 2: return "Allowed (Limited)" case 3: return "Allowed (One Time)" case 4: return "Denied (System)" case 5: return "Allowed (System)" default: return "Unknown (\(authValue))" } } var statusColor: Color { switch authValue { case 0: return .red case 1: return .green case 2: return .orange case 3: return .blue case 4: return .red case 5: return .green default: return .gray } } var reasonText: String? { guard let reason = authReason else { return nil } return TCCReasonMapper.friendlyReason(for: reason) } } // MARK: - TCC Query Result /// Results from querying both User and System TCC databases struct TCCQueryResult { var userPermissions: [TCCPermission] = [] var systemPermissions: [TCCPermission] = [] var userError: String? var systemError: String? var hasUserPermissions: Bool { !userPermissions.isEmpty } var hasSystemPermissions: Bool { !systemPermissions.isEmpty } var hasAnyPermissions: Bool { hasUserPermissions || hasSystemPermissions } var allPermissions: [TCCPermission] { let combined = userPermissions + systemPermissions return combined.sorted { $0.displayName < $1.displayName } } } // MARK: - Service Name Mapper /// Maps TCC service identifiers to user-friendly names enum TCCServiceMapper { static func friendlyName(for service: String) -> String { let mapping: [String: String] = [ // System Policies "kTCCServiceSystemPolicyAllFiles": "Full Disk Access", "kTCCServiceSystemPolicyAppBundles": "App Management", "kTCCServiceSystemPolicyAppData": "App Data", "kTCCServiceSystemPolicyDesktopFolder": "Desktop Folder", "kTCCServiceSystemPolicyDocumentsFolder": "Documents Folder", "kTCCServiceSystemPolicyDownloadsFolder": "Downloads Folder", "kTCCServiceSystemPolicyNetworkVolumes": "Network Volumes", "kTCCServiceSystemPolicyRemovableVolumes": "Removable Volumes", // Security & Monitoring "kTCCServiceAccessibility": "Accessibility", "kTCCServicePostEvent": "Input Monitoring", "kTCCServiceListenEvent": "Input Monitoring", "kTCCServiceEndpointSecurityClient": "Endpoint Security", "kTCCServiceScreenCapture": "Screen Recording", // Hardware & Sensors "kTCCServiceCamera": "Camera", "kTCCServiceMicrophone": "Microphone", "kTCCServiceLocation": "Location Services", // Personal Data "kTCCServiceAddressBook": "Contacts", "kTCCServiceCalendar": "Calendar", "kTCCServiceReminders": "Reminders", "kTCCServicePhotos": "Photos", "kTCCServiceMediaLibrary": "Apple Music", // Communication "kTCCServiceBluetoothAlways": "Bluetooth", "kTCCServiceWillow": "Home", // Other "kTCCServiceAppleEvents": "Automation", "kTCCServiceFileProviderPresence": "File Provider Presence" ] return mapping[service] ?? service } } // MARK: - Reason Code Mapper /// Maps TCC auth_reason codes to user-friendly explanations enum TCCReasonMapper { static func friendlyReason(for reason: Int) -> String { switch reason { case 1: return "User consent" case 2: return "User denied" case 3: return "Service policy" case 4: return "MDM policy" case 5: return "Override" case 6: return "Missing usage string" case 7: return "Prompt timeout" case 8: return "Preflight unknown" case 9: return "Entitled" case 10: return "App type policy" default: return "Reason \(reason)" } } } ================================================ FILE: Pearcleaner/Logic/TCC/TCCQueryHelper.swift ================================================ // // TCCQueryHelper.swift // Pearcleaner // // SQLite3 query helper for TCC (Transparency, Consent, and Control) databases // import Foundation import SQLite3 // MARK: - TCC Query Helper class TCCQueryHelper { /// Query a TCC database for permissions granted to a specific bundle ID /// - Parameters: /// - dbPath: Path to the TCC.db file /// - bundleIdentifier: Bundle ID to query (e.g., "com.apple.Safari") /// - source: The source database (user or system) /// - Returns: Result containing array of permissions or an error static func queryTCCDatabase( dbPath: String, bundleIdentifier: String, source: TCCPermission.PermissionSource ) -> Result<[TCCPermission], Error> { var db: OpaquePointer? var permissions: [TCCPermission] = [] // Open database in read-only mode guard sqlite3_open_v2( dbPath, &db, SQLITE_OPEN_READONLY, nil ) == SQLITE_OK else { let errorMsg = String(cString: sqlite3_errmsg(db)) sqlite3_close(db) return .failure(TCCError.databaseOpenFailed(errorMsg)) } defer { sqlite3_close(db) } // Query with explicit column selection (defensive against schema changes) // Only get bundle ID entries (client_type = 0), not path entries (client_type = 1) let query = """ SELECT service, auth_value, auth_reason, last_modified FROM access WHERE client_type = 0 AND client = ? ORDER BY service ASC """ var statement: OpaquePointer? guard sqlite3_prepare_v2(db, query, -1, &statement, nil) == SQLITE_OK else { let errorMsg = String(cString: sqlite3_errmsg(db)) return .failure(TCCError.queryPreparationFailed(errorMsg)) } defer { sqlite3_finalize(statement) } // Bind bundle identifier parameter (prevents SQL injection) sqlite3_bind_text(statement, 1, (bundleIdentifier as NSString).utf8String, -1, nil) // Execute query and collect results while sqlite3_step(statement) == SQLITE_ROW { // Column 0: service (TEXT) let servicePtr = sqlite3_column_text(statement, 0) let service = servicePtr != nil ? String(cString: servicePtr!) : "" // Column 1: auth_value (INTEGER) let authValue = Int(sqlite3_column_int(statement, 1)) // Column 2: auth_reason (INTEGER, nullable) let authReason: Int? if sqlite3_column_type(statement, 2) != SQLITE_NULL { authReason = Int(sqlite3_column_int(statement, 2)) } else { authReason = nil } // Column 3: last_modified (INTEGER unix epoch, nullable) let lastModified: Date? if sqlite3_column_type(statement, 3) != SQLITE_NULL { let timestamp = sqlite3_column_int64(statement, 3) lastModified = Date(timeIntervalSince1970: TimeInterval(timestamp)) } else { lastModified = nil } let permission = TCCPermission( service: service, authValue: authValue, authReason: authReason, lastModified: lastModified, source: source ) permissions.append(permission) } return .success(permissions) } /// Query both User and System TCC databases /// - Parameter bundleIdentifier: Bundle ID to query /// - Returns: Combined results from both databases static func queryAllDatabases( bundleIdentifier: String ) async -> TCCQueryResult { var result = TCCQueryResult() // User database path let home = FileManager.default.homeDirectoryForCurrentUser.path let userDBPath = "\(home)/Library/Application Support/com.apple.TCC/TCC.db" // System database path (requires Full Disk Access) let systemDBPath = "/Library/Application Support/com.apple.TCC/TCC.db" // Query user database let userResult = queryTCCDatabase(dbPath: userDBPath, bundleIdentifier: bundleIdentifier, source: .user) switch userResult { case .success(let permissions): result.userPermissions = permissions case .failure(let error): result.userError = error.localizedDescription } // Query system database (may fail without FDA) let systemResult = queryTCCDatabase(dbPath: systemDBPath, bundleIdentifier: bundleIdentifier, source: .system) switch systemResult { case .success(let permissions): result.systemPermissions = permissions case .failure(let error): result.systemError = error.localizedDescription } return result } } // MARK: - TCC Error /// Errors that can occur when querying TCC databases enum TCCError: LocalizedError { case databaseOpenFailed(String) case queryPreparationFailed(String) var errorDescription: String? { switch self { case .databaseOpenFailed(let msg): return "Failed to open TCC database: \(msg)" case .queryPreparationFailed(let msg): return "Failed to prepare query: \(msg)" } } } ================================================ FILE: Pearcleaner/Logic/UndoHistoryManager.swift ================================================ // // UndoHistoryManager.swift // Pearcleaner // // Created by Alin Lupascu on 11/10/25. // import Foundation import SwiftUI import AlinFoundation // MARK: - UndoHistoryRecord struct UndoHistoryRecord: Codable, Identifiable { let id: UUID let timestamp: Date let appName: String let bundleFolderPath: String let filePairs: [(original: String, trashed: String)] let fileCount: Int enum CodingKeys: String, CodingKey { case id, timestamp, appName, bundleFolderPath, filePairs, fileCount } // Custom encoding/decoding for tuples init(id: UUID = UUID(), timestamp: Date, appName: String, bundleFolderPath: String, filePairs: [(String, String)], fileCount: Int) { self.id = id self.timestamp = timestamp self.appName = appName self.bundleFolderPath = bundleFolderPath self.filePairs = filePairs self.fileCount = fileCount } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) id = try container.decode(UUID.self, forKey: .id) timestamp = try container.decode(Date.self, forKey: .timestamp) appName = try container.decode(String.self, forKey: .appName) bundleFolderPath = try container.decode(String.self, forKey: .bundleFolderPath) fileCount = try container.decode(Int.self, forKey: .fileCount) let pairs = try container.decode([[String]].self, forKey: .filePairs) filePairs = pairs.map { ($0[0], $0[1]) } } func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(id, forKey: .id) try container.encode(timestamp, forKey: .timestamp) try container.encode(appName, forKey: .appName) try container.encode(bundleFolderPath, forKey: .bundleFolderPath) try container.encode(fileCount, forKey: .fileCount) let pairs = filePairs.map { [$0.original, $0.trashed] } try container.encode(pairs, forKey: .filePairs) } } // MARK: - UndoHistoryManager @MainActor class UndoHistoryManager: ObservableObject { static let shared = UndoHistoryManager() @Published private(set) var history: [UndoHistoryRecord] = [] private let maxHistoryCount = 10 private let historyFileURL: URL private init() { // Store in Application Support folder let appSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] let pearcleanerDir = appSupport.appendingPathComponent("Pearcleaner") // Create directory if needed try? FileManager.default.createDirectory(at: pearcleanerDir, withIntermediateDirectories: true) historyFileURL = pearcleanerDir.appendingPathComponent("UndoHistory.json") loadHistory() } // MARK: - Public Methods /// Add a new delete operation to history func addRecord(appName: String, bundleFolderPath: String, filePairs: [(String, String)]) { let record = UndoHistoryRecord( timestamp: Date(), appName: appName, bundleFolderPath: bundleFolderPath, filePairs: filePairs, fileCount: filePairs.count ) // Insert at beginning (most recent first) history.insert(record, at: 0) // Keep only last 10 if history.count > maxHistoryCount { history = Array(history.prefix(maxHistoryCount)) } saveHistory() } /// Restore selected records from history func restoreRecords(_ records: [UndoHistoryRecord]) async throws { for record in records { // Validate bundle folder still exists guard FileManager.default.fileExists(atPath: record.bundleFolderPath) else { printOS("⚠️ Skipping restore for \(record.appName) - bundle folder no longer exists") continue } // Build file pairs from stored paths let filePairs: [(trashURL: URL, originalURL: URL)] = record.filePairs.map { (trashURL: URL(fileURLWithPath: $0.trashed), originalURL: URL(fileURLWithPath: $0.original)) } // Restore using FileManagerUndo (runs synchronously via semaphore) let success = FileManagerUndo.shared.restoreFiles(filePairs: filePairs) if !success { printOS("⚠️ Failed to restore files for \(record.appName)") throw NSError(domain: "UndoHistoryManager", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to restore \(record.appName)"]) } // Remove from history after successful restore if let index = history.firstIndex(where: { $0.id == record.id }) { history.remove(at: index) } } saveHistory() } /// Remove a specific record by bundle folder path (called when user does Cmd+Z undo) func removeRecord(bundleFolderPath: String) { if let index = history.firstIndex(where: { $0.bundleFolderPath == bundleFolderPath }) { history.remove(at: index) saveHistory() } } /// Remove stale entries where bundle folder no longer exists in trash func cleanupStaleEntries() { let initialCount = history.count history.removeAll { record in !FileManager.default.fileExists(atPath: record.bundleFolderPath) } if history.count < initialCount { saveHistory() } } /// Check if a record's files still exist in trash func isRecordValid(_ record: UndoHistoryRecord) -> Bool { return FileManager.default.fileExists(atPath: record.bundleFolderPath) } // MARK: - Persistence private func saveHistory() { do { let encoder = JSONEncoder() encoder.dateEncodingStrategy = .iso8601 encoder.outputFormatting = .prettyPrinted let data = try encoder.encode(history) try data.write(to: historyFileURL, options: .atomic) } catch { printOS("❌ Failed to save undo history: \(error.localizedDescription)") } } private func loadHistory() { guard FileManager.default.fileExists(atPath: historyFileURL.path) else { return } do { let data = try Data(contentsOf: historyFileURL) let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 history = try decoder.decode([UndoHistoryRecord].self, from: data) // Cleanup stale entries on load cleanupStaleEntries() } catch { printOS("❌ Failed to load undo history: \(error.localizedDescription)") } } } ================================================ FILE: Pearcleaner/Logic/UndoManager.swift ================================================ // // UndoManager.swift // Pearcleaner // // Created by Alin Lupascu on 2/24/25. // import Foundation import SwiftUI import AlinFoundation class FileManagerUndo { // MARK: - Singleton Instance static let shared = FileManagerUndo() // Private initializer to enforce singleton pattern private init() {} // NSUndoManager instance to handle undo/redo actions let undoManager = UndoManager() // MARK: - Path Validation /// Validates that a path is safe to delete (not a critical system path or app folder) private func validatePath(_ path: String) -> Bool { // Normalize path let normalizedPath = URL(fileURLWithPath: path).standardizedFileURL.path // Block empty paths guard !normalizedPath.trimmingCharacters(in: .whitespaces).isEmpty else { printOS("⚠️ Blocked deletion: Empty path") return false } // Combine critical system paths + user app folder paths into single set let criticalSystemPaths = [ "/", "/Applications", "/Library", "/System", "/usr", "/bin", "/sbin", "/etc", "/var", "/private", "/opt", NSHomeDirectory() ] let userAppPaths = FolderSettingsManager.shared.folderPaths let blockedPaths = Set(criticalSystemPaths + userAppPaths) // Block if path exactly matches any blocked path if blockedPaths.contains(normalizedPath) { printOS("⚠️ Blocked deletion: Protected path '\(normalizedPath)'") return false } return true } func deleteFiles(at urls: [URL], isCLI: Bool = false, bundleName: String? = nil) -> Bool { // Filter out invalid/dangerous paths before deletion let validURLs = urls.filter { validatePath($0.path) } // If no valid paths remain, return early guard !validURLs.isEmpty else { printOS("⚠️ All paths were blocked - no files deleted") return false } // Log if any paths were filtered out if validURLs.count < urls.count { printOS("⚠️ Filtered out \(urls.count - validURLs.count) dangerous path(s)") } let trashPath = (NSHomeDirectory() as NSString).appendingPathComponent(".Trash") let dispatchSemaphore = DispatchSemaphore(value: 0) // Semaphore to make it synchronous var finalStatus = false // Store the final success/failure status var tempFilePairs: [(trashURL: URL, originalURL: URL)] = [] var seenFileNames: [String: Int] = [:] let hasProtectedFiles = validURLs.contains { $0.isProtected } // Create bundle folder name with app name and timestamp let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy-MM-dd_HH-mm-ss" let timestamp = dateFormatter.string(from: Date()) let folderName: String if let customBundleName = bundleName { folderName = customBundleName } else if !AppState.shared.appInfo.appName.isEmpty { folderName = AppState.shared.appInfo.appName } else { // Fallback for plugins: use the first file's name or "Mixed Files" if let firstFile = validURLs.first { folderName = firstFile.deletingPathExtension().lastPathComponent } else { folderName = "Mixed Files" } } let bundleFolderName = "\(folderName)_\(timestamp)" let bundleFolderPath = (trashPath as NSString).appendingPathComponent(bundleFolderName) let bundleFolderURL = URL(fileURLWithPath: bundleFolderPath) // Create the bundle folder first let createFolderCommand = "/bin/mkdir -p \"\(bundleFolderPath)\"" let mvCommands = validURLs.map { file -> String in let baseName = file.lastPathComponent var count = seenFileNames[baseName] ?? 0 var finalName = baseName // Check for duplicate names within the bundle folder repeat { if count > 0 { finalName = "\(baseName)-\(count)" } count += 1 } while FileManager.default.fileExists(atPath: (bundleFolderPath as NSString).appendingPathComponent(finalName)) seenFileNames[baseName] = count let destinationURL = bundleFolderURL.appendingPathComponent(finalName) tempFilePairs.append((trashURL: destinationURL, originalURL: file)) let source = "\"\(file.path)\"" let destination = "\"\(destinationURL.path)\"" return "/bin/mv \(source) \(destination)" }.joined(separator: " ; ") // Combine folder creation and file moves let finalCommands = "\(createFolderCommand) ; \(mvCommands)" let filePairs = tempFilePairs if executeFileCommands(finalCommands, isCLI: isCLI, hasProtectedFiles: hasProtectedFiles) { undoManager.registerUndo(withTarget: self) { target in let result = target.restoreFiles(filePairs: filePairs) if !result { printOS("Trash Error: Could not restore files.") } } undoManager.setActionName("Delete File") // Record in persistent history Task { @MainActor in UndoHistoryManager.shared.addRecord( appName: folderName, bundleFolderPath: bundleFolderPath, filePairs: filePairs.map { ($0.originalURL.path, $0.trashURL.path) } ) } // Play trash sound after successful deletion if !isCLI { playTrashSound() } finalStatus = true } else { // printOS("Trash Error: \(isCLI ? "Could not run commands directly with sudo." : "Could not perform privileged commands.")") updateOnMain { AppState.shared.trashError = true } finalStatus = false } dispatchSemaphore.signal() dispatchSemaphore.wait() return finalStatus } func restoreFiles(filePairs: [(trashURL: URL, originalURL: URL)], isCLI: Bool = false) -> Bool { let dispatchSemaphore = DispatchSemaphore(value: 0) var finalStatus = true let hasProtectedFiles = filePairs.contains { $0.originalURL.deletingLastPathComponent().isProtected } let commands = filePairs.map { let source = "\"\($0.trashURL.path)\"" let destination = "\"\($0.originalURL.path)\"" return "/bin/mv \(source) \(destination)" }.joined(separator: " ; ") // Determine the bundle folder to clean up after restore var bundleFolderToRemove: String? = nil if let firstFilePair = filePairs.first { let bundleFolder = firstFilePair.trashURL.deletingLastPathComponent() // Only remove if it looks like our generated bundle folder (contains underscore for timestamp) if bundleFolder.lastPathComponent.contains("_") { bundleFolderToRemove = bundleFolder.path } } // Add bundle folder cleanup command if we have one to remove let finalCommands = if let bundleFolder = bundleFolderToRemove { "\(commands) ; /bin/rmdir \"\(bundleFolder)\"" } else { commands } if executeFileCommands(finalCommands, isCLI: isCLI, hasProtectedFiles: hasProtectedFiles, isRestore: true) { // Remove from persistent history after successful restore if let bundleFolder = bundleFolderToRemove { Task { @MainActor in UndoHistoryManager.shared.removeRecord(bundleFolderPath: bundleFolder) } } finalStatus = true } else { // printOS("Trash Error: \(isCLI ? "Failed to run restore CLI commands" : "Failed to run restore privileged commands")") updateOnMain { AppState.shared.trashError = true } finalStatus = false } dispatchSemaphore.signal() dispatchSemaphore.wait() return finalStatus } // Helper function to perform shell commands based on available privileges private func executeFileCommands(_ commands: String, isCLI: Bool, hasProtectedFiles: Bool, isRestore: Bool = false) -> Bool { let semaphore = DispatchSemaphore(value: 0) var status = false // Try privileged wrapper (helper tool or Authorization Services) Task { let result = try! await runSUCommand( commands, errorContext: isRestore ? "Undo restore operation failed" : "Undo delete operation failed", throwOnFailure: false ) status = result.0 if !status { printOS(isRestore ? "Restore Error: \(result.1)" : "Trash Error: \(result.1)") updateOnMain { AppState.shared.trashError = true } // Fallback to direct shell if appropriate if isCLI || !hasProtectedFiles { let shellResult = runDirectShellCommand(command: commands) status = shellResult.0 if !status { printOS(isRestore ? "Restore Error: \(shellResult.1)" : "Trash Error: \(shellResult.1)") updateOnMain { AppState.shared.trashError = true } } } } semaphore.signal() } semaphore.wait() return status } // Helper to run direct non-privileged shell commands private func runDirectShellCommand(command: String) -> (Bool, String) { let task = Process() task.launchPath = "/bin/sh" task.arguments = ["-c", command] let pipe = Pipe() task.standardOutput = pipe task.standardError = pipe task.launch() task.waitUntilExit() let data = pipe.fileHandleForReading.readDataToEndOfFile() let output = String(data: data, encoding: .utf8) ?? "" if output.lowercased().contains("permission denied") { return (false, output) } return (task.terminationStatus == 0, output) } } extension URL { var isProtected: Bool { !FileManager.default.isWritableFile(atPath: self.path) } } ================================================ FILE: Pearcleaner/Logic/Utilities.swift ================================================ // // Utilities.swift // Pearcleaner // // Created by Alin Lupascu on 11/3/23. // import Foundation import SwiftUI import AlinFoundation import AppKit import AudioToolbox import OpenDirectory func ifOSBelow(macOS major: Int, _ minor: Int = 0, _ patch: Int = 0) -> Bool { if !ProcessInfo.processInfo.isOperatingSystemAtLeast( OperatingSystemVersion(majorVersion: major, minorVersion: minor, patchVersion: patch) ) { return true } else { return false } } func playTrashSound(undo: Bool = false) { let soundName = undo ? "poof item off dock.aif" : "drag to trash.aif" let path = "/System/Library/Components/CoreAudio.component/Contents/SharedSupport/SystemSounds/dock/\(soundName)" let url = URL(fileURLWithPath: path) var soundID: SystemSoundID = 0 AudioServicesCreateSystemSoundID(url as CFURL, &soundID) AudioServicesPlaySystemSound(soundID) } // Check if pear symlink exists func checkCLISymlink() -> Bool { let filePath = "/usr/local/bin/pear" let fileManager = FileManager.default guard fileManager.fileExists(atPath: filePath) else { return false } do { let destination = try fileManager.destinationOfSymbolicLink(atPath: filePath) return destination == Bundle.main.executablePath } catch { return false } } // Fix legacy pearcleaner symlink if it exists func fixLegacySymlink() { let legacyPath = "/usr/local/bin/pearcleaner" let fileManager = FileManager.default if fileManager.fileExists(atPath: legacyPath) { manageSymlink(install: false, symlinkName: "pearcleaner") manageSymlink(install: true, symlinkName: "pear") } } // Install/uninstall symlink for CLI func manageSymlink(install: Bool, symlinkName: String = "pear") { @AppStorage("settings.general.cli") var isCLISymlinked = false guard let appPath = Bundle.main.executablePath else { printOS("Error: Unable to get the executable path.") return } let symlinkPath = "/usr/local/bin/\(symlinkName)" let symlinkExists = FileManager.default.fileExists(atPath: symlinkPath) let binPathExists = directoryExists(at: "/usr/local/bin") if install && symlinkExists { printOS("Symlink already exists at \(symlinkPath). No action needed.") return } if !install && !symlinkExists { printOS("Symlink does not exist at \(symlinkPath). No action needed.") return } // Prepare privileged commands var command = "" if install { // Create the /usr/local/bin directory if it doesn't exist, then create symlink if !binPathExists { command += "mkdir -p /usr/local/bin && " } command += "ln -s '\(appPath)' '\(symlinkPath)'" } else { // Remove the symlink using FileManagerUndo for safe trash deletion let _ = FileManagerUndo.shared.deleteFiles(at: [URL(fileURLWithPath: symlinkPath)], bundleName: "CLI-Symlink") return } // Perform privileged commands using unified wrapper (only for creating symlink) Task { let result = try! await runSUCommand( command, errorContext: "Failed to create CLI symlink", throwOnFailure: false ) if !result.0 { printOS("Symlink creation failed: \(result.1)") } updateOnMain { isCLISymlinked = checkCLISymlink() } } } func directoryExists(at path: String) -> Bool { let fileManager = FileManager.default return fileManager.fileExists(atPath: path, isDirectory: nil) } /// Clean up all stale /tmp/pearcleaner* directories /// Used before creating new temp directories to avoid conflicts from previous failed operations func cleanupPearcleanerTempDirs() { let tmpDir = URL(fileURLWithPath: "/tmp") guard let contents = try? FileManager.default.contentsOfDirectory( at: tmpDir, includingPropertiesForKeys: nil ) else { return } for item in contents where item.lastPathComponent.hasPrefix("pearcleaner") { try? FileManager.default.removeItem(at: item) } } // Open trash folder func openTrash() { if let trashURL = try? FileManager.default.url(for: .trashDirectory, in: .userDomainMask, appropriateFor: nil, create: false) { NSWorkspace.shared.open(trashURL) } } // Check if restricted app func isRestricted(atPath path: URL) -> Bool { if path.path.contains("/Applications/Safari") || path.path.contains(Bundle.main.name) || path.path.contains("/Applications/Utilities") { return true } else { return false } } // Check app bundle architecture func checkAppBundleArchitecture(at appBundlePath: String) -> Arch { return autoreleasepool { let bundleURL = URL(fileURLWithPath: appBundlePath) let infoPlistURL = bundleURL.appendingPathComponent("Contents/Info.plist") // Read Info.plist in autoreleasepool to release immediately let executableName: String? = autoreleasepool { guard let infoDict = NSDictionary(contentsOf: infoPlistURL) as? [String: Any] else { return nil } return infoDict["CFBundleExecutable"] as? String } guard let execName = executableName else { return .empty } let executableURL = bundleURL.appendingPathComponent("Contents/MacOS").appendingPathComponent(execName) // Use FileHandle to read only the header bytes needed for architecture detection guard let fileHandle = try? FileHandle(forReadingFrom: executableURL) else { return .empty } defer { try? fileHandle.close() } // Read first 8 bytes for magic and initial header guard let headerData = try? fileHandle.read(upToCount: 8), headerData.count >= 4 else { return .empty } // Check for fat (universal) binary let magic = headerData.prefix(4).withUnsafeBytes { $0.load(as: UInt32.self).bigEndian } let FAT_MAGIC: UInt32 = 0xcafebabe if magic == FAT_MAGIC { // Fat binary - read architecture count guard headerData.count >= 8 else { return .empty } let numArchs = headerData.subdata(in: 4..<8).withUnsafeBytes { $0.load(as: UInt32.self).bigEndian } // Read all architecture headers at once (20 bytes each) let archHeadersSize = Int(numArchs) * 20 guard let archHeadersData = try? fileHandle.read(upToCount: archHeadersSize), archHeadersData.count == archHeadersSize else { return .empty } var archs: [Arch] = [] var offset = 0 for _ in 0..= 8 else { return .empty } // Check magic number for 64-bit Mach-O let magic = headerData.prefix(4).withUnsafeBytes { $0.load(as: UInt32.self) } if magic == 0xfeedfacf || magic == 0xcffaedfe { // 64-bit Mach-O - read CPU type let cputypeLittle = headerData.subdata(in: 4..<8).withUnsafeBytes { $0.load(as: UInt32.self) } let cputypeBig = headerData.subdata(in: 4..<8).withUnsafeBytes { $0.load(as: UInt32.self).bigEndian } // ARM64 detection if cputypeLittle == 0x0100000c || cputypeBig == 0x0c000001 { return .arm } // x86_64 detection else if cputypeLittle == 0x01000007 || cputypeBig == 0x07000001 { return .intel } } else if magic == 0xfeedface || magic == 0xcefaedfe { // 32-bit Mach-O (less common) let cputypeLittle = headerData.subdata(in: 4..<8).withUnsafeBytes { $0.load(as: UInt32.self) } let cputypeBig = headerData.subdata(in: 4..<8).withUnsafeBytes { $0.load(as: UInt32.self).bigEndian } // ARM64 and x86_64 with 32-bit magic (edge case) if cputypeLittle == 0x0100000c || cputypeBig == 0x0c000001 { return .arm } else if cputypeLittle == 0x01000007 || cputypeBig == 0x07000001 { return .intel } } return .empty } } } // Main function that now directly uses the Mach-O helper func thinAppBundleArchitecture(at appBundlePath: URL, of arch: Arch, multi: Bool = false, dryRun: Bool = false, showAlert: Bool = true) -> (Bool, [String: UInt64]?) { // Reset bundle size to 0 before starting (only for real thinning) if !dryRun { updateOnMain { if let index = AppState.shared.sortedApps.firstIndex(where: { $0.path == appBundlePath }) { var updatedAppInfo = AppState.shared.sortedApps[index] updatedAppInfo.bundleSize = 0 AppState.shared.sortedApps[index] = updatedAppInfo } } } // Use privileged helper if installed and needed, otherwise fallback to bundle thinning var success: Bool var sizes: [String: UInt64]? if dryRun { // For dry run, always use direct calculation without helper tools let result = thinAppBundle(at: appBundlePath, dryRun: true) success = result.0 sizes = result.1 } else if HelperToolManager.shared.isHelperToolInstalled { // Use privileged bundle thinning - helper handles the entire bundle with elevated privileges let semaphore = DispatchSemaphore(value: 0) success = false sizes = nil Task { let result = await HelperToolManager.shared.runBundleThinning(bundlePath: appBundlePath.path) success = result.0 if result.0, !result.2.isEmpty { sizes = result.2 } semaphore.signal() } semaphore.wait() if !success { // Helper tool failed, fallback to bundle thinning let result = thinAppBundle(at: appBundlePath) success = result.0 sizes = result.1 } } else { // No helper tool installed, use comprehensive bundle thinning let result = thinAppBundle(at: appBundlePath) success = result.0 sizes = result.1 } // Update the app bundle timestamp to refresh Finder (only for real thinning) if success && !dryRun { if !multi { // Update app sizes after lipo in sortedApps array and the AppInfo active object AppState.shared.getBundleSize(for: AppState.shared.appInfo) { newSize in let newFileSize = totalSizeOnDisk(for: AppState.shared.appInfo.path) updateOnMain { // Create a new appInfo instance with updated size values var updatedAppInfo = AppState.shared.appInfo updatedAppInfo.bundleSize = newSize updatedAppInfo.fileSize[AppState.shared.appInfo.path] = newFileSize updatedAppInfo.arch = isOSArm() ? .arm : .intel // Replace the whole appInfo object AppState.shared.appInfo = updatedAppInfo // Show savings information if we have size data if showAlert { if let bundleSizes = sizes, let preSize = bundleSizes["pre"], let postSize = bundleSizes["post"] { let savingsPercentage = Int((Double(preSize - postSize) / Double(preSize)) * 100) let title = String(format: NSLocalizedString("Space Savings: %d%%", comment: "Lipo result title"), savingsPercentage) let message = String(format: NSLocalizedString("Bundle thinning complete.\nTotal space saved from all binaries in bundle.", comment: "Lipo result message")) showCustomAlert(title: title, message: message, style: .informational) } } } } } else { // Update the appInfo in sortedApps array let calculatedSize = totalSizeOnDisk(for: appBundlePath) DispatchQueue.main.async { // Update the array if let index = AppState.shared.sortedApps.firstIndex(where: { $0.path == appBundlePath }) { var updatedAppInfo = AppState.shared.sortedApps[index] updatedAppInfo.bundleSize = calculatedSize updatedAppInfo.arch = isOSArm() ? .arm : .intel AppState.shared.sortedApps[index] = updatedAppInfo } } } } return (success, sizes) } // Check if app is running before deleting app files func killApp(appId: String) async { let runningApps = NSWorkspace.shared.runningApplications for app in runningApps { if app.bundleIdentifier == appId { app.terminate() } } } //MARK: Broken on 13.0 // Open app settings //func openAppSettings() { // if #available(macOS 14.0, *) { // @Environment(\.openSettings) var openSettings // openSettings() // } else { // NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) // } //} func openAppSettingsWindow(tab: CurrentTabView? = nil, updater: Updater) { // Determine which tab to open: // 1. If caller explicitly passes a tab (not nil), use it and save as new preference // 2. Otherwise, check for saved tab preference // 3. If no saved preference, default to .general if let requestedTab = tab { // Explicit tab request - use it and save as new preference UserDefaults.standard.set(requestedTab.rawValue, forKey: "settings.general.selectedTab") } else if UserDefaults.standard.object(forKey: "settings.general.selectedTab") == nil { // No saved preference - default to general and save it UserDefaults.standard.set(CurrentTabView.general.rawValue, forKey: "settings.general.selectedTab") } // Otherwise, use the existing saved preference (no need to set it again) // Note: Tab changes during use are handled by @AppStorage in SettingsView // Create SettingsView with environment objects let settingsView = SettingsView() .environmentObject(AppState.shared) .environmentObject(Locations()) .environmentObject(FolderSettingsManager.shared) .environmentObject(updater) .environmentObject(PermissionManager.shared) .frame(width: 800, height: 710) .navigationTitle("") // Open using WindowManager WindowManager.shared.open( id: "settings", with: settingsView, width: 800, height: 710, resizable: false, toolbarStyle: .unified ) } // Get user profile picture struct UserProfile { let firstName: String? let image: NSImage? } func getUserProfile() async -> UserProfile { // Use Task.detached to completely break QoS inheritance and escalation // This prevents the system from escalating to match the caller's QoS await Task.detached(priority: .medium) { do { let session = ODSession.default() let node = try ODNode(session: session, type: UInt32(kODNodeTypeLocalNodes)) let record = try node.record( withRecordType: kODRecordTypeUsers, name: NSUserName(), attributes: ["dsAttrTypeStandard:RealName", kODAttributeTypeJPEGPhoto] ) // First name var firstName: String? = nil if let realName = (try? record.values(forAttribute: "dsAttrTypeStandard:RealName") as? [String])?.first { firstName = realName.components(separatedBy: " ").first } // JPEG photo var resizedImage: NSImage? = nil if let dataList = try? record.values(forAttribute: kODAttributeTypeJPEGPhoto) as? [Data], let data = dataList.first, let img = NSImage(data: data) { let targetSize = NSSize(width: 50, height: 50) // Pre-render resized image (must run on main thread) // Note: NSImage Sendable conformance requires macOS 14+, but we ensure thread safety via DispatchQueue.main resizedImage = await withCheckedContinuation { continuation in DispatchQueue.main.async { let resized = NSImage(size: targetSize, flipped: false) { rect in img.draw(in: rect, from: NSRect(origin: .zero, size: img.size), operation: .copy, fraction: 1.0) return true } continuation.resume(returning: resized) } } } return UserProfile(firstName: firstName, image: resizedImage) } catch { printOS("Failed fetching user profile: \(error)") return UserProfile(firstName: nil, image: nil) } }.value } // Check if file/folder name has localized variant func showLocalized(url: URL) -> String { guard FileManager.default.fileExists(atPath: url.path) else { return url.lastPathComponent } do { // Retrieve the localized name let resourceValues = try url.resourceValues(forKeys: [.localizedNameKey]) if let localizedName = resourceValues.localizedName { return localizedName } } catch { printOS("Error retrieving localized name: \(error)") } // Return the last path component as a fallback return url.lastPathComponent } extension URL { func localizedName() -> String { do { let resourceValues = try self.resourceValues(forKeys: [.localizedNameKey]) return resourceValues.localizedName?.replacingOccurrences(of: ".app", with: "") ?? self.lastPathComponent.replacingOccurrences(of: ".app", with: "") } catch { printOS("Error getting localized name: \(error)") return self.lastPathComponent.replacingOccurrences(of: ".app", with: "") } } } extension String { func localizedName() -> String { let url = URL(fileURLWithPath: self) do { let resourceValues = try url.resourceValues(forKeys: [.localizedNameKey]) return resourceValues.localizedName?.replacingOccurrences(of: ".app", with: "") ?? self } catch { printOS("Error getting localized name: \(error)") return self } } } extension String { func pathWithArrows(separatorColor: Color = .secondary, separatorFont: Font = .caption) -> some View { let components = self.dropFirst().components(separatedBy: "/").filter { !$0.isEmpty } return HStack(spacing: 4) { ForEach(Array(components.enumerated()), id: \.offset) { index, component in Group { Text(component) if index < components.count - 1 { Image(systemName: "chevron.right") .font(separatorFont) .foregroundStyle(separatorColor) } } .lineLimit(1) .truncationMode(.tail) } } } } extension URL { /// Returns the bundle name of the container by its UUID if found. func containerNameByUUID() -> String { // Extract the last path component, which should be the UUID let uuid = self.lastPathComponent // Ensure the UUID matches the expected pattern. let uuidRegex = try! NSRegularExpression( pattern: "^[0-9A-F]{8}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{4}-[0-9A-F]{12}$", options: .caseInsensitive ) let range = NSRange(location: 0, length: uuid.utf16.count) guard uuidRegex.firstMatch(in: uuid, options: [], range: range) != nil else { // printOS("The URL does not point to a valid UUID container.") return "" } // Path to the Containers directory. let containersPath = FileManager.default.homeDirectoryForCurrentUser .appendingPathComponent("Library/Containers") do { // List all directories in the Containers folder. let containerDirectories = try FileManager.default.contentsOfDirectory( at: containersPath, includingPropertiesForKeys: nil, options: .skipsHiddenFiles ) // Iterate over each directory to find a match with the UUID. for directory in containerDirectories { let directoryName = directory.lastPathComponent if directoryName == uuid { // Attempt to read the metadata plist file. let metadataPlistURL = directory.appendingPathComponent(".com.apple.containermanagerd.metadata.plist") if let metadataDict = NSDictionary(contentsOf: metadataPlistURL), let applicationBundleID = metadataDict["MCMMetadataIdentifier"] as? String { return applicationBundleID } } } } catch { printOS("Error accessing the Containers directory: \(error)") } // Return nil if no matching UUID is found. return "" } } // Removes the sidebar toggle button from the toolbar, if running on macOS 14.0 or newer. extension View { @ViewBuilder func removeSidebarToggle() -> some View { if #available(macOS 14.0, *) { self.toolbar(removing: .sidebarToggle) } else { self } } } // Drag window by background extension View { // Helper function to apply the movable background window func movableByWindowBackground() -> some View { self.background(MovableWindowAccessor()) } } // Custom NSWindow accessor to modify window properties struct MovableWindowAccessor: NSViewRepresentable { func makeNSView(context: Context) -> NSView { let nsView = NSView() DispatchQueue.main.async { if let window = nsView.window { // Enable dragging by the window's background window.isMovableByWindowBackground = true } } return nsView } func updateNSView(_ nsView: NSView, context: Context) {} } // Return image for different folders func folderImages(for path: String) -> AnyView? { @Environment(\.colorScheme) var colorScheme if path.contains("/Library/Containers/") || path.contains("/Library/Group Containers/") { return AnyView( Image(systemName: "shippingbox.fill") .resizable() .scaledToFit() .frame(width: 13) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText.opacity(0.5)) .help("Container") ) } else if path.contains("/Library/Application Scripts/") { return AnyView( Image(systemName: "applescript.fill") .resizable() .scaledToFit() .frame(width: 13) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText.opacity(0.5)) .help("Application Script") ) } else if path.contains(".plist") { return AnyView( Image(systemName: "doc.badge.gearshape.fill") .resizable() .scaledToFit() .frame(width: 13) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText.opacity(0.5)) .help("Plist File") ) } // Return nil if no conditions are met return nil } // Check if app bundle is nested func isNested(path: URL) -> Bool { let applicationsPath = "/Applications" let homeApplicationsPath = "\(home)/Applications" guard path.path.contains("Applications") else { return false } // Get the parent directory of the app let parentDirectory = path.deletingLastPathComponent().path // Check if the parent directory is not directly /Applications or ~/Applications return parentDirectory != applicationsPath && parentDirectory != homeApplicationsPath } // Date formatter for metadata func formattedMDDate(from date: Date) -> String { let formatter = DateFormatter() formatter.dateStyle = .long formatter.timeZone = .current // Use the current timezone return formatter.string(from: date) } // --- Extend String to remove periods, spaces and lowercase the string extension String { func pearFormat() -> String { // Optimized version: directly build result string without intermediate array var result = "" result.reserveCapacity(self.count) // Pre-allocate to avoid reallocation // Iterate unicode scalars and append alphanumerics directly for scalar in self.unicodeScalars { if CharacterSet.alphanumerics.contains(scalar) { result.unicodeScalars.append(scalar) } } // Lowercase the result result = result.lowercased() // If the result is empty after processing, return the original string // to avoid false matches with empty string comparisons return result.isEmpty ? self : result } } // --- Returns comma separated string as array of strings extension String { func toConditionFormat() -> [String] { if self.isEmpty { return [] } return self.components(separatedBy: ",").map { $0.trimmingCharacters(in: .whitespaces) } } } func sendStartNotificationFW() { DistributedNotificationCenter.default().postNotificationName(Notification.Name("Pearcleaner.StartFileWatcher"), object: nil, userInfo: nil, deliverImmediately: true) } func sendStopNotificationFW() { DistributedNotificationCenter.default().postNotificationName(Notification.Name("Pearcleaner.StopFileWatcher"), object: nil, userInfo: nil, deliverImmediately: true) } func formatRelativeTime(_ date: Date) -> String { let formatter = RelativeDateTimeFormatter() formatter.unitsStyle = .abbreviated return formatter.localizedString(for: date, relativeTo: Date()) } /// Generate system and Pearcleaner environment information string for debugging func getSystemDebugString() -> String { let processInfo = ProcessInfo.processInfo let osVersion = processInfo.operatingSystemVersionString let osVersionParts = processInfo.operatingSystemVersion let pcVersion = Bundle.main.version let pcBuild = Bundle.main.buildVersion let pcBundleID = Bundle.main.bundleIdentifier ?? "unknown" // Architecture info #if arch(arm64) let archRunning = "arm64 (Apple Silicon)" #elseif arch(x86_64) let archRunning = "x86_64 (Intel)" #else let archRunning = "unknown" #endif // Memory info let physicalMemory = processInfo.physicalMemory let memoryFormatted = formatBytes(Int64(physicalMemory)) // Processor info let processorCount = processInfo.processorCount let activeProcessorCount = processInfo.activeProcessorCount // System uptime let uptime = processInfo.systemUptime let uptimeFormatted = formatTimeInterval(uptime) // Pearcleaner memory usage var taskInfo = mach_task_basic_info() var count = mach_msg_type_number_t(MemoryLayout.size)/4 let result = withUnsafeMutablePointer(to: &taskInfo) { $0.withMemoryRebound(to: integer_t.self, capacity: 1) { task_info(mach_task_self_, task_flavor_t(MACH_TASK_BASIC_INFO), $0, &count) } } let memoryUsage = result == KERN_SUCCESS ? formatBytes(Int64(taskInfo.resident_size)) : "unknown" // Full Disk Access status let hasFullDiskAccess = FileManager.default.isReadableFile(atPath: "/Library/Application Support/com.apple.TCC/TCC.db") return """ ==================================== System & Pearcleaner Debug Info ==================================== Pearcleaner Version: \(pcVersion) (Build \(pcBuild)) Bundle ID: \(pcBundleID) Running Architecture: \(archRunning) Memory Usage: \(memoryUsage) ==================================== macOS Version: \(osVersion) macOS Build: \(osVersionParts.majorVersion).\(osVersionParts.minorVersion).\(osVersionParts.patchVersion) ==================================== System Memory: \(memoryFormatted) Processor Cores: \(processorCount) total, \(activeProcessorCount) active System Uptime: \(uptimeFormatted) ==================================== Full Disk Access: \(hasFullDiskAccess ? "✓ Granted" : "✗ Not Granted") ==================================== Timestamp: \(Date().description) ==================================== """ } /// Format time interval into human-readable string private func formatTimeInterval(_ interval: TimeInterval) -> String { let days = Int(interval) / 86400 let hours = (Int(interval) % 86400) / 3600 let minutes = (Int(interval) % 3600) / 60 if days > 0 { return "\(days)d \(hours)h \(minutes)m" } else if hours > 0 { return "\(hours)h \(minutes)m" } else { return "\(minutes)m" } } /// Format bytes into human-readable string func formatBytes(_ bytes: Int64) -> String { let formatter = ByteCountFormatter() formatter.allowedUnits = [.useAll] formatter.countStyle = .file return formatter.string(fromByteCount: bytes) } // MARK: - Unified Privileged Command Execution /// Unified wrapper for executing privileged commands with automatic fallback /// Tries helper tool first if installed, falls back to Authorization Services (password prompt) func runSUCommand( _ command: String, errorContext: String? = nil, skipHelperCheck: Bool = false, throwOnFailure: Bool = false ) async throws -> (success: Bool, output: String) { // Pattern 1: Skip helper check (for CLI diagnostics) if skipHelperCheck { let result = await HelperToolManager.shared.runCommand(command, skipHelperCheck: true) return result } // Pattern 2: Try helper if installed if HelperToolManager.shared.isHelperToolInstalled { let result = await HelperToolManager.shared.runCommand(command) if result.0 { return result } // Helper failed, log if errorContext provided if let context = errorContext { printOS("\(context): \(result.1)") } } // Pattern 3: Fallback to Authorization Services (prompts for password) let (success, output) = performPrivilegedCommands(commands: command) // Log custom error if provided and command failed if !success, let context = errorContext { printOS("\(context): \(output)") } // Throw error if requested if throwOnFailure && !success { throw NSError(domain: "SU Command", code: 1, userInfo: [NSLocalizedDescriptionKey: output]) } return (success, output) } ================================================ FILE: Pearcleaner/PearcleanerApp.swift ================================================ // // PearcleanerApp.swift // Pearcleaner // // Created by Alin Lupascu on 10/31/23. // import SwiftUI import AppKit import AlinFoundation @main struct PearcleanerApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate //MARK: ObservedObjects @ObservedObject var appState = AppState.shared @ObservedObject private var permissionManager = PermissionManagerLocal.shared @ObservedObject private var helperToolManager = HelperToolManager.shared //MARK: StateObjects @StateObject var locations = Locations() @StateObject var fsm = FolderSettingsManager.shared @StateObject private var updater = Updater(owner: "alienator88", repo: "Pearcleaner") init() { //MARK: GUI or CLI launch mode. handleLaunchMode() //MARK: Initialize password request handler for SUDO_ASKPASS IPC _ = PasswordRequestHandler.shared //MARK: Pre-load apps data during app initialization (use streaming for fast initial load) let folderPaths = FolderSettingsManager.shared.folderPaths loadApps(folderPaths: folderPaths, useStreaming: true) //MARK: Pre-load volume information AppState.shared.loadVolumeInfo() } var body: some Scene { WindowGroup { MainWindow() .environmentObject(appState) .environmentObject(locations) .environmentObject(fsm) .environmentObject(updater) .environmentObject(permissionManager) } .windowStyle(.hiddenTitleBar) .windowToolbarStyle(.unified) .windowResizability(.contentMinSize) .commands { AppCommands(appState: appState, locations: locations, fsm: fsm, updater: updater) } } } // MARK: - App Delegate class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } func applicationDidFinishLaunching(_ notification: Notification) { NSWindow.allowsAutomaticWindowTabbing = false // Register as services provider (required for NSServices to work) NSApp.servicesProvider = self // Check permissions once at launch PermissionManagerLocal.shared.checkPermissions(types: [.fullDiskAccess]) { results in PermissionManagerLocal.shared.results = results } // Load and cleanup undo history Task { @MainActor in UndoHistoryManager.shared.cleanupStaleEntries() } ensureApplicationSupportFolderExists() cleanupPearcleanerTempDirs() } func applicationWillTerminate(_ notification: Notification) {} func applicationShouldRestoreApplicationState(_ app: NSApplication) -> Bool { return false } // MARK: - Service Handler @objc func handleServiceRequest(_ pasteboard: NSPasteboard, userData: NSString, error: AutoreleasingUnsafeMutablePointer) { // Get file URLs from pasteboard guard let fileURLs = pasteboard.readObjects(forClasses: [NSURL.self], options: [ .urlReadingFileURLsOnly: true ]) as? [URL], !fileURLs.isEmpty else { printOS("Service: No valid file URLs found in pasteboard") return } // Process all selected .app files let appURLs = fileURLs.filter { $0.pathExtension == "app" } guard !appURLs.isEmpty else { printOS("Service: No .app bundles found in selection") return } // Open deep link for each app - DeeplinkManager will queue and process them sequentially for appURL in appURLs { if let deepLinkURL = URL(string: "pear://com.alienator88.Pearcleaner?path=\(appURL.path.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? appURL.path)") { NSWorkspace.shared.open(deepLinkURL) } } } } ================================================ FILE: Pearcleaner/Resources/Assets.xcassets/AccentColor.colorset/Contents.json ================================================ { "colors" : [ { "color" : { "color-space" : "display-p3", "components" : { "alpha" : "1.000", "blue" : "0xE8", "green" : "0x64", "red" : "0x16" } }, "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "color" : { "color-space" : "display-p3", "components" : { "alpha" : "1.000", "blue" : "0xE8", "green" : "0x64", "red" : "0x16" } }, "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Pearcleaner/Resources/Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "compression-type" : "gpu-optimized-smallest" } } ================================================ FILE: Pearcleaner/Resources/Glass.icon/icon.json ================================================ { "fill" : { "linear-gradient" : [ "display-p3:0.93159,0.94081,0.94081,1.00000", "display-p3:0.75900,0.75900,0.75900,1.00000" ] }, "groups" : [ { "blur-material-specializations" : [ { "value" : null }, { "appearance" : "tinted", "value" : null } ], "layers" : [ { "blend-mode" : "normal", "fill" : "none", "glass-specializations" : [ { "value" : false }, { "appearance" : "dark", "value" : true }, { "appearance" : "tinted", "value" : true } ], "hidden" : false, "image-name" : "pear.heic", "name" : "pear", "position" : { "scale" : 1.5, "translation-in-points" : [ 11.3046875, 3.296875 ] } } ], "shadow-specializations" : [ { "value" : { "kind" : "neutral", "opacity" : 0.5 } }, { "appearance" : "tinted", "value" : { "kind" : "neutral", "opacity" : 0.5 } } ], "specular" : true, "translucency-specializations" : [ { "value" : { "enabled" : true, "value" : 0.5 } }, { "appearance" : "tinted", "value" : { "enabled" : true, "value" : 0.5 } } ] } ], "supported-platforms" : { "squares" : [ "macOS" ] } } ================================================ FILE: Pearcleaner/Resources/Info.plist ================================================ CFBundleDocumentTypes CFBundleTypeName Application Bundle CFBundleTypeRole Viewer LSHandlerRank Default LSItemContentTypes com.apple.application-bundle CFBundleGetInfoString CFBundleURLTypes CFBundleTypeRole Editor CFBundleURLName com.alienator88.Pearcleaner CFBundleURLSchemes pear NSServices NSMenuItem default Uninstall with Pearcleaner NSMessage handleServiceRequest NSSendFileTypes com.apple.application-bundle NSRequiredContext ================================================ FILE: Pearcleaner/Resources/Localizable.xcstrings ================================================ { "sourceLanguage" : "en", "strings" : { "" : { "comment" : "Do not translate", "shouldTranslate" : false }, "(Build %@)" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "(Build %@)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "(Compilación %@)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "(Version %@)" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "(Pembuatan %@)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "(Costruire %@)" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "(ビルド %@)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "(Build %@)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "(Kompilacja %@)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "(Compilação %@)" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "(Build %@)" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "(Сборка %@)" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "(Verzia %@)" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "(Build %@)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "(Sürüm %@)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "(Збірка %@)" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "(Bản dựng %@)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "(Build %@)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "(版號 %@)" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "(版號 %@)" } } } }, "(Hidden)" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "(Versteckt)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "(Hidden)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "(Hidden)" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "(Hidden)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "(Hidden)" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "(Hidden)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "(Hidden)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "(Hidden)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "(Hidden)" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "(Hidden)" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "(Hidden)" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "(Hidden)" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "(Hidden)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "(Hidden)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "(Hidden)" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "(Hidden)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "(Hidden)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "(Hidden)" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "(Hidden)" } } } }, "(same unit)" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "(same unit)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "(same unit)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "(same unit)" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "(same unit)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "(stessa unità)" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "(same unit)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "(동일 단위)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "(same unit)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "(same unit)" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "(mesma unidade)" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "(same unit)" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "(same unit)" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "(ista enota)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "(same unit)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "(same unit)" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "(cùng đơn vị)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "(same unit)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "(same unit)" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "(same unit)" } } } }, "(Skipped %@)" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "(Übersprungen %@)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "(Omitido %@)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "(Ignoré %@)" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "(Dilewati %@)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "(Saltato %@)" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "(スキップ済み %@)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "(건너뛴 %@)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "(Pominięto %@)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "(Ignorado %@)" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "(Ignorado %@)" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "(Пропущено %@)" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "(Preskočené %@)" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "(Preskočeno %@)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "(Atlandı %@)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "(Пропущено %@)" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "(Bỏ qua %@)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "(已跳过 %@)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "(已跳過 %@)" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "(已跳過 %@)" } } } }, "**%lld%%** savings" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "**%lld%%** savings" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "**%lld%%** de ahorro" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "**%lld%%** de gain" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "**%lld%%** penghematan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "**%lld%%** di risparmio" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%lld%% の節約" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "**%lld%%** 를 절약" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "**%lld%%** oszczędności" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "**%lld%%** de economia" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "**%lld%%** de economia" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Экономия **%lld%%**" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "**%lld%%** úspor" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "**%lld%%** prihranki" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "**%lld%%** tasarruf" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "**%lld%%** збережено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "**%lld%%** đang lưu" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "**%lld%%** 瘦身" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "已節省 **%lld%%**" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "已節省 **%lld%%**" } } } }, "**whoami** command should return 'root' if helper is running correctly." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Der Befehl **whoami** sollte 'root' zurückgeben, wenn der Helfer korrekt läuft." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "El comando **whoami** debería devolver 'root' si el asistente está funcionando correctamente." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "La commande **whoami** devrait retourner 'root' si l'assistant fonctionne correctement." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Perintah **whoami** harus mengembalikan 'root' jika pembantu berjalan dengan benar." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Il comando **whoami** dovrebbe restituire 'root' se l'helper è in esecuzione correttamente." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "**whoami** コマンドは、ヘルパーが正しく動作している場合に 'root' を返すべきです。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "**whoami** 명령은 도우미가 올바르게 실행되고 있으면 'root'를 반환해야 합니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Polecenie **whoami** powinno zwrócić 'root', jeśli pomocnik działa poprawnie." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "O comando **whoami** deve retornar 'root' se o assistente estiver funcionando corretamente." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "O comando **whoami** deve retornar 'root' se o assistente estiver a funcionar corretamente." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Команда **whoami** должна возвращать 'root', если помощник работает правильно." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Príkaz **whoami** by mal vrátiť 'root', ak pomocník beží správne." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ukaz **whoami** bi moral vrniti 'root', če pomočnik deluje pravilno." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "**whoami** komutu, yardımcı doğru çalışıyorsa 'root' döndürmelidir." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Команда **whoami** повинна повертати 'root', якщо помічник працює правильно." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Lệnh **whoami** nên trả về 'root' nếu trình trợ giúp đang chạy đúng cách." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "**whoami** 命令应返回 'root',如果助手运行正常。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "**whoami** 命令應返回 'root',如果助手運行正常。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "**whoami** 命令應返回 'root',如果助手運行正常。" } } } }, "%@ is already on the latest release available" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%@ ist bereits die neuesten Version" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%@ ya está en la última versión disponible" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%@ a déjà la dernière version disponible" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "%@ sudah menggunakan rilis terbaru yang tersedia" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%@ è già sulla versione più recente disponibile" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%@ は既に最新バージョンです" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@은(는) 이미 사용 가능한 최신 릴리즈입니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%@ jest już dostępny w najnowszej wersji" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%@ já está na versão mais recente disponível" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "%@ já está na última versão disponível" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "%@ уже обновлен до последней доступной версии" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "%@ je už na najnovšej dostupnej verzii" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "%@ je že na najnovejši razpoložljivi izdaji" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%@ halihazırda mevcut olan en son sürümde" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "%@ вже на останній версії" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "%@ hiện đang là phiên bản mới nhất" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%@ 已经是最新版本" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%@ 已經為最新版本" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "%@ 已經為最新版本" } } } }, "%@ will check for updates" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%@ wird nach Aktualisierungen suchen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%@ buscará actualizaciones" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Fréquence de vérification de mise à jour de %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "%@ akan memeriksa pembaruan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%@ controllerà gli aggiornamenti" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%@ はアップデートを確認します" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@가 업데이트를 확인할 것입니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%@ sprawdzi dostępność aktualizacji" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%@ verificará atualizações" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "%@ irá verificar atualizações" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "%@ проверит наличие обновлений" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "%@ skontroluje aktualizácie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "%@ bo preveril posodobitve" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%@ güncellemeleri şu sıklıkla kontrol edecek" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "%@ перевірить наявність оновлень" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "%@ sẽ kiểm tra bản cập nhật" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%@ 将检查更新" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%@ 會檢查更新" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "%@ 會檢查更新" } } } }, "%lld app%@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%lld App%@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%lld aplicación%@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%lld app%@ " } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "%lld aplikasi%@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%lld app%@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%lld アプリ%@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%lld 앱" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%lld aplikacja%@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%lld aplicativo%@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "%lld app%@ " } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "%lld приложение%@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "%lld aplikácia%@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "%lld aplikacija%@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%lld uygulama%@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "%lld додаток%@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "%lld ứng dụng%@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%lld 个应用%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%lld 應用程式%@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "%lld 應用程式%@" } } } }, "%lld file%@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%lld Datei%@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%lld archivo%@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%lld fichier%@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "%lld berkas%@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%lld file%@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%lld ファイル%@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%lld 파일" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%lld plik%@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%lld arquivo%@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "%lld ficheiro%@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "%lld файл%@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "%lld súbor%@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "%lld datoteka%@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%lld dosya%@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "%lld файл%@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "%lld tệp%@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%lld 文件%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%lld 檔案%@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "%lld 檔案%@" } } } }, "%lld files" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%lld Dateien" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%lld archivos" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%lld fichiers" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "%lld berkas" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%lld file" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%lld 個のファイル" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%lld개 파일" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%lld plików" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%lld arquivos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "%lld ficheiros" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "%lld файлов" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "%lld súborov" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "%lld datotek" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%lld dosya" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "%lld файлів" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "%lld tệp" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%lld 个文件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%lld 個檔案" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "%lld 個檔案" } } } }, "%lld loaded" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%lld geladen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%lld cargado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%lld chargé(e)s" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "%lld dimuat" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%lld caricato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%lld個の読み込みが完了しました" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%lld 로드됨" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%lld załadowanych" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%lld carregados" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "%lld carregado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "%lld загружено" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "%lld načítaných" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "%lld naloženo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%lld yüklendi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "%lld завантажено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "%lld đã tải xong" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%lld 已加载" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%lld 已載入" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "%lld 已載入" } } } }, "%lld not loaded" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%lld nicht geladen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%lld no cargado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%lld non chargé(e)s" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "%lld tidak dimuat" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%lld non caricato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%lld が読み込まれていません" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%lld 로드되지 않음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%lld niezaładowanych" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%lld não carregado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "%lld não carregado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "%lld не загружено" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "%lld nenačítaných" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "%lld ni naloženo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%lld yüklenmedi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "%lld не завантажено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "%lld chưa được tải" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%lld 未加载" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%lld 未載入" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "%lld 未載入" } } } }, "%lld of %lld file%@ selected" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld von %2$lld Dateien%3$@ ausgewählt" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "%1$lld of %2$lld file%3$@ selected" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Seleccionados %1$lld de %2$lld archivos%3$@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld sur %2$lld fichier%3$@ sélectionné" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld dari %2$lld file%3$@ terpilih" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld di %2$lld file%3$@ selezionati" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld / %2$lld ファイル%3$@が選択されました" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld개 중 %2$lld개의 파일%3$@ 선택됨" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wybrano %1$lld z %2$lld plików%3$@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld de %2$lld arquivo%3$@ selecionado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld de %2$lld ficheiro%3$@ selecionado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Выбрано %1$lld из %2$lld файлов%3$@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld z %2$lld súboru%3$@ vybratých" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izbranih %1$lld od %2$lld datotek%3$@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld / %2$lld dosya%3$@ seçildi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Вибрано %1$lld з %2$lld файлів%3$@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld trong số %2$lld tệp%3$@ đã được chọn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "已选择 %1$lld 个文件,共 %2$lld 个文件%3$@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "已選擇 %1$lld 個檔案中的 %2$lld 個%3$@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "已選擇 %1$lld 個檔案中的 %2$lld 個%3$@" } } } }, "%lld of %lld language%@ selected for removal" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld von %2$lld Sprache%3$@ zur Entfernung ausgewählt" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "%1$lld of %2$lld language%3$@ selected for removal" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld de %2$lld idioma%3$@ seleccionado para eliminar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld sur %2$lld langue%3$@ sélectionnée pour suppression" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld dari %2$lld bahasa%3$@ dipilih untuk dihapus" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld di %2$lld lingua%3$@ selezionata per la rimozione" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%2$lld 言語のうち %1$lld 言語%3$@ が削除対象に選ばれました" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "제거를 위해 %lld개 중 %lld개 언어%@ 선택됨" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld z %2$lld języka%3$@ wybrano do usunięcia" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld de %2$lld idioma%3$@ selecionado para remoção" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld de %2$lld idioma%3$@ selecionado para remoção" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld из %2$lld языков%3$@ выбрано для удаления" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld z %2$lld jazykov%3$@ vybraných na odstránenie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld od %2$lld jezika%3$@ izbranih za odstranitev" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%2$lld dilden %1$lld dil%3$@ kaldırılmak üzere seçildi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld з %2$lld мови%3$@ вибрано для видалення" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld trong số %2$lld ngôn ngữ%3$@ đã chọn để xóa" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "已选择移除 %1$lld 个中的 %2$lld 种语言%3$@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld 個中的 %2$lld 個語言%3$@ 已選擇移除" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld 個中的 %2$lld 個語言%3$@ 已選擇移除" } } } }, "%lld outdated" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%lld veraltet" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%lld desactualizado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%lld obsolète" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "%lld usang" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%lld obsoleti" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%lld が古くなっています" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%lld개 업데이트 필요" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%lld nieaktualne" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%lld desatualizado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "%lld desatualizado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "%lld устаревших" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "%lld zastaraných" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "%lld zastarelo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%lld güncel değil" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "%lld застарілий" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "%lld lỗi thời" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%lld 已过时" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%lld 過時" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "%lld 已過時" } } } }, "%lld package%@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%lld Paket%@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%lld paquete%@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%lld package%@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "%lld paket%@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%lld pacchetto%@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%lld パッケージ%@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%lld 패키지" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%lld pakiet%@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%lld pacote%@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "%lld pacote%@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "%lld пакет%@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "%lld balík%@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "%lld paket%@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%lld paket%@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "%lld пакет%@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "%lld gói%@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%lld 个包%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%lld 個套件%@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "%lld 個套件%@" } } } }, "%lld package%@ installed" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld Paket%2$@ installiert" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "%1$lld package%2$@ installed" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld paquete%2$@ instalado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld paquet%2$@ installé" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld paket%2$@ terpasang" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld pacchetto%2$@ installato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld パッケージ%2$@ がインストールされました" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld 패키지%2$@ 설치됨" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld pakiet%2$@ zainstalowany" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld pacote%2$@ instalado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld pacote%2$@ instalado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld пакет%2$@ установлен" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld balík%2$@ nainštalovaný" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld paket%2$@ nameščen" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld paket%2$@ yüklendi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld пакет%2$@ встановлено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld gói%2$@ đã được cài đặt" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld 个软件包%2$@ 已安装" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld 個套件%2$@ 已安裝" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld 個套件%2$@ 已安裝" } } } }, "%lld path%@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%lld Pfad%@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%lld ruta%@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%lld chemin%@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "%lld jalur%@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%lld percorso%@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%lld パス%@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%lld 경로" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%lld ścieżka%@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%lld caminho%@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "%lld caminho%@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "%lld путь%@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "%lld cesta%@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "%lld pot%@ " } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%lld yol%@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "%lld шлях%@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "%lld đường dẫn%@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%lld 路径%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%lld 路徑%@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "%lld 路徑%@" } } } }, "%lld plugin%@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%lld Plugin%@ " } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%lld complemento%@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%lld plugin%@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "%lld plugin%@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%lld plugin%@ " } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%lld プラグイン%@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%lld 플러그인" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%lld wtyczka%@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%lld plugin%@ " } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "%lld plugin%@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "%lld плагин%@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "%lld plugin%@ " } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "%lld vtičnik%@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%lld eklenti%@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "%lld плагін%@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "%lld plugin%@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%lld 插件%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%lld 插件%@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "%lld 插件%@" } } } }, "%lld related file%@ found" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld zugehörige Datei%2$@ gefunden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld archivo relacionado%2$@ encontrado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld fichier lié%2$@ trouvé" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld file terkait%2$@ ditemukan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%lld file correlati%@ trovati" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld 関連ファイル%2$@ が見つかりました" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%lld개의 관련 파일 발견됨" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Znaleziono %1$lld powiązany plik%2$@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld arquivo relacionado%2$@ encontrado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "%lld ficheiro%@ relacionado encontrado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Найдено %1$lld связанный файл%2$@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nájdený %1$lld súvisiaci súbor%2$@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "%lld povezana datoteka%@ najdena" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld ilgili dosya%2$@ bulundu" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Знайдено %1$lld пов'язаний файл%2$@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld tệp liên quan%2$@ được tìm thấy" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "找到 %1$lld 个相关文件%2$@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "找到 %1$lld 個相關檔案%2$@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "找到 %1$lld 個相關檔案%2$@" } } } }, "%lld result%@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%lld Ergebnis%@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%lld resultado%@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%lld résultat%@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "%lld hasil%@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%lld risultato%@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%lld 件の結果%@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%lld 결과" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%lld wynik%@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%lld resultado%@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "%lld resultado%@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "%lld результат%@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "%lld výsledok%@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "%lld rezultat%@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%lld sonuç%@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "%lld результат%@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "%lld kết quả%@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%lld 个结果%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%lld 結果%@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "%lld 結果%@" } } } }, "%lld schedule%@ active" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld Zeitplan%2$@ aktiv" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "%1$lld schedule%2$@ active" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld horario%2$@ activo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld programme%2$@ actif" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld jadwal%2$@ aktif" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld programma%2$@ attivo" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld スケジュール%2$@ アクティブ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld 일정%2$@ 활성화" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld harmonogram%2$@ aktywny" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld agenda%2$@ ativa" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld agenda%2$@ ativa" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld расписание%2$@ активно" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld plán%2$@ aktívny" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld urnik%2$@ aktiven" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld program%2$@ etkin" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld розклад%2$@ активний" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld lịch trình%2$@ đang hoạt động" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld 日程%2$@ 活动" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld 行程%2$@ 活動" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld 時間表%2$@ 啟動" } } } }, "%lld service%@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%lld Dienst%@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%lld servicio%@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%lld service%@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "%lld layanan%@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%lld servizio%@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%lld サービス%@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%lld 서비스" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%lld serwis%@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%lld serviço%@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "%lld serviço%@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "%lld служба%@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "%lld služba%@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "%lld storitev%@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%lld hizmet%@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "%lld служба%@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "%lld dịch vụ%@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%lld 服务%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%lld 服務%@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "%lld 服務%@" } } } }, "%lld valid %@ found" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%lld gültige %@ gefunden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%lld %@ válidos encontrados" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%lld %@ valides trouvées" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "%lld valid %@ ditemukan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%lld %@ validi trovati" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%lld 個の有効な %@ が見つかりました" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%lld개의 유효한 %@ 발견됨" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%lld ważny %@ został znaleziony" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%lld %@ válidos encontrados" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "%lld %@ válido encontrado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "%lld действительных %@ найдено" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "%lld platných %@ nájdených" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "%lld veljavnih %@ najdenih" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%lld geçerli %@ bulundu" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "%lld дійсних %@ знайдено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đã tìm thấy %lld %@ hợp lệ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%lld 个有效的 %@ 找到" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%lld 個有效的 %@ 找到" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "%lld 個有效的 %@ 已找到" } } } }, "%lld/%lld file%@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld/%2$lld Datei%3$@" } }, "en" : { "stringUnit" : { "state" : "new", "value" : "%1$lld/%2$lld file%3$@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld/%2$lld archivo%3$@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld/%2$lld fichier%3$@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld/%2$lld berkas%3$@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld/%2$lld file%3$@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld/%2$lld ファイル%3$@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld/%2$lld 파일%3$@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld/%2$lld plik%3$@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld/%2$lld arquivo%3$@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld/%2$lld ficheiro%3$@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld/%2$lld файл%3$@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld/%2$lld súbor%3$@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld/%2$lld datoteka%3$@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld/%2$lld dosya%3$@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld/%2$lld файл%3$@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld/%2$lld tệp%3$@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld/%2$lld 文件%3$@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld/%2$lld 檔案%3$@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "%1$lld/%2$lld 檔案%3$@" } } } }, "• Auto-updates" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "• Automatische Aktualisierungen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "• Actualizaciones automáticas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "• Mises à jour automatiques" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "• Pembaruan otomatis" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "• Aggiornamenti automatici" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "• 自動更新" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "• 자동 업데이트" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "• Aktualizacje automatyczne" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "• Atualizações automáticas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "• Atualizações automáticas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "• Автообновления" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "• Automatické aktualizácie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "• Samodejne posodobitve" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "• Otomatik güncellemeler" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "• Автоматичні оновлення" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "• Cập nhật tự động" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "• 自动更新" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "• 自動更新" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "• 自動更新" } } } }, "⚙️ Service/Daemon" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "⚙️ Dienst/Daemon" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "⚙️ Servicio/Daemon" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "⚙️ Service/Daemon" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "⚙️ Layanan/Daemon" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "⚙️ Servizio/Daemon" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "⚙️ サービス/デーモン" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "⚙️ 서비스/데몬" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "⚙️ Usługa/Demon" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "⚙️ Serviço/Daemon" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "⚙️ Serviço/Daemon" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "⚙️ Сервис/Демон" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "⚙️ Služba/Daemon" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "⚙️ Storitev/Daemon" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "⚙️ Hizmet/Daemon" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "⚙️ Сервіс/Демон" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "⚙️ Dịch vụ/Daemon" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "⚙️ 服务/守护进程" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "⚙️ 服務/守護程式" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "⚙️ 服務/守護程式" } } } }, "⚠️ Conflicts With" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ Konflikte mit" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ Conflictos con" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ Conflits avec" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ Konflik dengan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ Conflitti con" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ 競合しています" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ 충돌" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ Konflikty z" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ Conflitos com" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ Conflitos Com" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ Конфликты с" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ Konflikty s" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ Konflikti z" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ Çakışmalar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ Конфлікти з" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ Xung đột với" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ 与...冲突" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ 與...衝突" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ 與...衝突" } } } }, "⚠️ Deprecated" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ Veraltet" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ Obsoleto" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ Obsolète" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ Usang" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ Obsoleto" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ 非推奨" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ 사용 중단됨" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ Przestarzałe" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ Obsoleto" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ Obsoleto" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ Устарело" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ Zastarané" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ Zastarelo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ Kullanım dışı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ Застаріле" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ Không còn sử dụng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ 已弃用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ 已棄用" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "⚠️ 已棄用" } } } }, "✅ Installed" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "✅ Installiert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "✅ Instalado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "✅ Installé" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "✅ Terinstal" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "✅ Installato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "✅ インストール済み" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "✅ 설치됨" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "✅ Zainstalowano" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "✅ Instalado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "✅ Instalado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "✅ Установлено" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "✅ Nainštalované" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "✅ Nameščeno" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "✅ Yüklendi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "✅ Встановлено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "✅ Đã cài đặt" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "✅ 已安装" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "✅ 已安裝" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "✅ 已安裝" } } } }, "💻 System Requirements" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "💻 Systemanforderungen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "💻 Requisitos del sistema" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "💻 Configuration système requise" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "💻 Persyaratan Sistem" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "💻 Requisiti di sistema" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "💻 システム要件" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "💻 시스템 요구 사항" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "💻 Wymagania systemowe" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "💻 Requisitos do sistema" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "💻 Requisitos do Sistema" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "💻 Системные требования" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "💻 Systémové požiadavky" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "💻 Sistemske zahteve" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "💻 Sistem Gereksinimleri" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "💻 Системні вимоги" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "💻 Yêu cầu hệ thống" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "💻 系统要求" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "💻 系統需求" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "💻 系統要求" } } } }, "📊 Popularity" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "📊 Beliebtheit" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "📊 Popularidad" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "📊 Popularité" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "📊 Popularitas" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "📊 Popolarità" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "📊 人気" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "📊 인기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "📊 Popularność" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "📊 Popularidade" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "📊 Popularidade" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "📊 Популярность" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "📊 Popularita" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "📊 Priljubljenost" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "📊 Popülerlik" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "📊 Популярність" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "📊 Phổ biến" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "📊 热度" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "📊 熱度" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "📊 熱度" } } } }, "📦 Recommended Replacements" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "📦 Empfohlene Ersatzteile" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "📦 Reemplazos recomendados" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "📦 Remplacements recommandés" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "📦 Penggantian yang Direkomendasikan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "📦 Sostituzioni Consigliate" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "📦 推奨交換品" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "📦 추천 교체" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "📦 Zalecane zamienniki" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "📦 Substituições Recomendadas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "📦 Substituições Recomendadas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "📦 Рекомендуемые замены" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "📦 Odporúčané náhrady" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "📦 Priporočene zamenjave" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "📦 Önerilen Değiştirmeler" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "📦 Рекомендовані заміни" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "📦 Thay thế được đề xuất" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "📦 推荐替换" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "📦 推薦替換" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "📦 推薦替換" } } } }, "🔒 Keg-Only" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "🔒 Nur-Fass" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "🔒 Solo barril" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "🔒 Fût uniquement" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "🔒 Hanya Tong" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "🔒 Solo Keg" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "🔒 樽のみ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "🔒 Keg-Only" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "🔒 Tylko beczka" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "🔒 Somente Barril" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "🔒 Apenas Barril" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "🔒 Только в кегах" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "🔒 Iba sud" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "🔒 Samo za sodčke" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "🔒 Sadece Fıçı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "🔒 Лише в кегах" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "🔒 Chỉ thùng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "🔒 仅限桶装" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "🔒 只限桶裝" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "🔒 只限桶裝" } } } }, "🚫 Disabled" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "🚫 Deaktiviert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "🚫 Desactivado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "🚫 Désactivé" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "🚫 Dinonaktifkan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "🚫 Disabilitato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "🚫 無効" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "🚫 비활성됨" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "🚫 Wyłączone" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "🚫 Desativado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "🚫 Desativado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "🚫 Отключено" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "🚫 Deaktivované" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "🚫 Onemogočeno" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "🚫 Devre Dışı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "🚫 Вимкнено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "🚫 Đã tắt" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "🚫 已禁用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "🚫 已停用" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "🚫 已停用" } } } }, "0 bytes" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "0 Bytes" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "0 bytes" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "0 octets" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "0 byte" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "0 byte" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "0 バイト" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "0 바이트" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "0 bajtów" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "0 bytes" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "0 bytes" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "0 байт" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "0 bajtov" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "0 bajtov" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "0 bayt" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "0 байт" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "0 byte" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "0 字节" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "0 位元組" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "0 字節" } } } }, "30 days" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "30 Tage" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "30 días" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "30 jours" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "30 hari" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "30 giorni" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "30日" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "30일" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "30 dni" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "30 dias" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "30 dias" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "30 дней" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "30 dní" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "30 dni" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "30 gün" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "30 днів" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "30 ngày" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "30天" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "30 天" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "30 日" } } } }, "90 days" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "90 Tage" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "90 días" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "90 jours" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "90 hari" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "90 giorni" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "90日" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "90일" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "90 dni" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "90 dias" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "90 dias" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "90 дней" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "90 dní" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "90 dni" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "90 gün" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "90 днів" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "90 ngày" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "90天" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "90天" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "90日" } } } }, "365 days" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "365 Tage" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "365 días" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "365 jours" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "365 hari" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "365 giorni" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "365日" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "365일" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "365 dni" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "365 dias" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "365 dias" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "365 дней" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "365 dní" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "365 dni" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "365 gün" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "365 днів" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "365 ngày" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "365天" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "365天" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "365天" } } } }, "A **huge** thank you to everyone who has contributed so far!" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ein **riesiges** Dankeschön an alle, die bisher mitgewirkt haben!" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Un **gran** agradecimiento a todos los que han contribuido hasta ahora!" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Un **grand** merci à tous ceux qui ont contribué jusqu'à présent !" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Terima kasih **banyak** kepada semua orang yang telah berkontribusi sejauh ini!" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Un **enorme** grazie a tutti coloro che hanno contribuito finora!" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "これまでに貢献してくれた皆さんに心から感謝します!" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "지금까지 기여해 주신 모든 분들께 **큰** 감사를 표합니다!" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "**Ogromne** podziękowania dla wszystkich, którzy dotychczas wnieśli swój wkład!" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Um **enorme** obrigado a todos que contribuíram até agora!" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Um **enorme** obrigado a todos que contribuíram até agora!" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Огромное спасибо всем, кто уже внес свой вклад!" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Obrovská vďaka všetkým, ktorí doteraz prispeli!" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ogromna hvala vsem, ki ste do zdaj prispevali!" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Şimdiye kadar katkıda bulunan herkese **çok** teşekkür ederiz!" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Величезна подяка усім, хто вже зробив свій внесок!" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Một **lời cảm ơn lớn** đến tất cả những ai đã đóng góp cho đến nay!" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "**非常感谢**迄今为止所有做出贡献的人!" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "**衷心感謝**所有參與貢獻的各位!" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "**衷心感謝**所有參與貢獻的各位!" } } } }, "A new version of Pearcleaner is available for download." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Eine neue Version von Pearcleaner steht zum Download bereit." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Una nueva versión de Pearcleaner está disponible para descargar." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Une nouvelle version de Pearcleaner est disponible au téléchargement." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Versi baru dari Pearcleaner tersedia untuk diunduh." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Una nuova versione di Pearcleaner è disponibile per il download." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner の新しいバージョンがダウンロード可能です。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner의 새 버전을 다운로드할 수 있습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nowa wersja Pearcleaner jest dostępna do pobrania." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Uma nova versão do Pearcleaner está disponível para download." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Uma nova versão do Pearcleaner está disponível para download." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Доступна новая версия Pearcleaner для загрузки." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nová verzia Pearcleaner je k dispozícii na stiahnutie." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Na voljo je nova različica Pearcleaner za prenos." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner'ın yeni bir sürümü indirilebilir durumda." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Доступна нова версія Pearcleaner для завантаження." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Một phiên bản mới của Pearcleaner có sẵn để tải xuống." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "可下载新版本的 Pearcleaner。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "可下載新版本的 Pearcleaner。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "可下載新版本的 Pearcleaner。" } } } }, "About" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Über" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Acerca de" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "À propos" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tentang" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Informazioni" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "このアプリについて" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "정보" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "O aplikacji" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Sobre" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Sobre" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "О приложении" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "O aplikácii" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "O programu" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hakkında" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Про програму" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thông tin ứng dụng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "关于" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "關於" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "關於" } } } }, "About %@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Über %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Acerca de %@\n" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "À propos de %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tentang %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Informazioni su %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%@について" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@ 정보" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "O %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Sobre %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Sobre %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "О программе %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "O aplikácii %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "O %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hakkında %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Про %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thông tin về %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "关于%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "關於 %@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "關於 %@" } } } }, "Accessibility" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Zugänglichkeit" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Accesibilidad" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Accessibilité" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Aksesibilitas" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Accessibilità" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アクセシビリティ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "접근성" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dostępność" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Acessibilidade" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Acessibilidade" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Специальные возможности" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Prístupnosť" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Dostopnost" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Erişilebilirlik" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Доступність" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Trợ năng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "辅助功能" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "輔助使用" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "輔助使用" } } } }, "Actions" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Aktionen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Acciones" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Actions" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tindakan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Azioni" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アクション" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "작업" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Akcje" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ações" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ações" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Действия" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Akcie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Dejanja" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Eylemler" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Дії" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hành động" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "操作" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "操作" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "操作" } } } }, "Actions (applied to all schedules)" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Aktionen (auf alle Zeitpläne angewendet)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Acciones (aplicadas a todos los horarios)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Actions (appliquées à tous les horaires)" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tindakan (diterapkan ke semua jadwal)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Azioni (applicate a tutti i programmi)" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アクション(すべてのスケジュールに適用)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "작업 (모든 일정에 적용됨)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Działania (stosowane do wszystkich harmonogramów)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ações (aplicadas a todos os horários)" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ações (aplicadas a todos os horários)" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Действия (применяются ко всем расписаниям)" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Akcie (použité na všetky plány)" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Dejanja (uporabljena za vse urnike)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Eylemler (tüm programlara uygulanır)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Дії (застосовуються до всіх розкладів)" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hành động (áp dụng cho tất cả các lịch trình)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "操作(适用于所有日程安排)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "操作(適用於所有日程安排)" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "操作(適用於所有日程安排)" } } } }, "Add" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Hinzufügen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Agregar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ajouter" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tambah" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiungi" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "追加" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "추가" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Добавить" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pridať" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ekle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Додати" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thêm" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "添加" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "新增" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "新增" } } } }, "Add a new schedule occurrence" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Einen neuen Zeitplan-Eintrag hinzufügen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Agregar una nueva ocurrencia de programación" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ajouter un nouvel événement au calendrier" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tambahkan kejadian jadwal baru" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiungi una nuova occorrenza di pianificazione" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "新しいスケジュールの発生を追加" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "새로운 일정 발생 추가" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj nowe wystąpienie harmonogramu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar uma nova ocorrência de agendamento" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar uma nova ocorrência de agendamento" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Добавить новое событие в расписание" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pridať novú udalosť v pláne" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj nov pojav urnika" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yeni bir program olayı ekle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Додати нову подію в розклад" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thêm một lần xuất hiện lịch trình mới" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "添加新的日程安排" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "新增排程事件" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "新增日程安排" } } } }, "Add a schedule to activate automatic updates" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Fügen Sie einen Zeitplan hinzu, um automatische Updates zu aktivieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Agrega un horario para activar las actualizaciones automáticas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ajoutez un planning pour activer les mises à jour automatiques" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tambahkan jadwal untuk mengaktifkan pembaruan otomatis" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiungi un programma per attivare gli aggiornamenti automatici" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "自動更新を有効にするためのスケジュールを追加" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "자동 업데이트를 활성화하려면 일정을 추가하세요" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj harmonogram, aby aktywować automatyczne aktualizacje" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Adicione um cronograma para ativar atualizações automáticas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar um agendamento para ativar atualizações automáticas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Добавьте расписание для активации автоматических обновлений" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pridajte plán na aktiváciu automatických aktualizácií" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Dodajte urnik za aktiviranje samodejnih posodobitev" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Otomatik güncellemeleri etkinleştirmek için bir program ekleyin" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Додайте розклад для активації автоматичних оновлень" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thêm lịch trình để kích hoạt cập nhật tự động" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "添加计划以激活自动更新" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "新增排程以啟用自動更新" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "添加時間表以啟用自動更新" } } } }, "Add a tap to get started" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Tippen Sie, um zu beginnen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Agrega un toque para comenzar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ajoutez une pression pour commencer" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tambahkan ketukan untuk memulai" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiungi un tocco per iniziare" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "タップを追加して開始" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "시작하려면 탭을 추가하세요" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj stuknięcie, aby rozpocząć" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Adicione um toque para começar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Adicione um toque para começar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Добавьте касание, чтобы начать" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pridajte ťuknutie na začiatok" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Dodajte tap, da začnete" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Başlamak için bir dokunuş ekleyin" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Додайте натиск, щоб почати" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thêm một lần chạm để bắt đầu" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "新增点击以开始" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "新增點擊以開始" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "新增點擊以開始" } } } }, "Add Comment Filter" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Kommentarfilter hinzufügen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Agregar filtro de comentarios" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ajouter un filtre de commentaires" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tambahkan Filter Komentar" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiungi filtro commenti" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "コメントフィルターを追加" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "댓글 필터 추가" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj filtr komentarzy" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar filtro de comentários" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar Filtro de Comentário" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Добавить фильтр комментариев" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pridať filter komentárov" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj filter za komentarje" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yorum Filtresi Ekle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Додати фільтр коментарів" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thêm bộ lọc bình luận" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "添加评论过滤器" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "新增評論篩選器" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "新增評論篩選器" } } } }, "Add Date Filter" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Datumsfilter hinzufügen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Agregar filtro de fecha" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ajouter un filtre de date" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tambahkan Filter Tanggal" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiungi filtro data" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "日付フィルターを追加" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "날짜 필터 추가" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj filtr daty" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar Filtro de Data" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar Filtro de Data" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Добавить фильтр по дате" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pridať filter dátumu" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj filter datuma" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Tarih Filtresi Ekle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Додати фільтр за датою" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thêm bộ lọc ngày" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "新增日期筛选器" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "新增日期篩選器" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "新增日期篩選器" } } } }, "Add Extension Filter" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Erweiterungsfilter hinzufügen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Agregar filtro de extensión" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ajouter un filtre d'extension" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tambahkan Filter Ekstensi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiungi filtro estensione" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "拡張フィルターを追加" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "확장 필터 추가" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj filtr rozszerzeń" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar filtro de extensão" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar Filtro de Extensão" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Добавить фильтр расширений" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pridať filter rozšírenia" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj filter razširitev" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uzantı Filtresi Ekle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Додати фільтр розширень" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thêm bộ lọc phần mở rộng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "添加扩展过滤器" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "添加擴展過濾器" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "添加擴展過濾器" } } } }, "Add file/folder" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ordner/Dateien hinzufügen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Agregar archivo/carpeta" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ajouter un fichier/dossier" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tambah file/folder" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiungi file/cartella" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ファイル/フォルダを追加" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "파일/폴더 추가" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj plik/folder" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar arquivo/pasta" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar ficheiro/pasta" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Добавить файл/папку" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pridať súbor/priečinok" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj datoteko/mapo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Dosya/klasör ekle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Додати файл/папку" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thêm tệp/thư mục" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "添加文件/文件夹" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "加入檔案/資料夾" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "加入檔案/資料夾" } } } }, "Add files or folders that will be ignored when searching for orphaned files. Click a path to remove it from the list." : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Fügen Sie Dateien oder Verzeichnisse hinzu, die bei der Suche nach verwaisten Dateien ignoriert werden sollen. Klicken Sie auf einen Pfad, um ihn aus der Liste zu entfernen." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Agrega archivos o carpetas que serán ignorados al buscar archivos huérfanos. Haz clic en una ruta para eliminarla de la lista." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ajouter des fichiers ou dossiers qui seront ignorés lors de la recherche de fichiers résiduels. Cliquer sur un chemin d’accès pour le retirer de la liste." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tambahkan file atau folder yang akan diabaikan saat mencari berkas yatim/berkas tanpa induk/berkas terlantar. Klik jalur untuk menghapusnya dari daftar." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiungi file o cartelle che verranno ignorati durante la ricerca di file orfani. Fai clic su un percorso per rimuoverlo dall'elenco." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "孤立ファイルの検索時に無視されるファイルやフォルダを追加します。パスをクリックするとリストから削除されます。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "연결되지 않은 파일을 검색할 때 무시할 파일이나 폴더를 추가합니다. 목록에서 제거하려면 경로를 클릭하세요." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj pliki lub foldery, które będą ignorowane podczas wyszukiwania osieroconych plików. Kliknij ścieżkę, aby usunąć ją z listy." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Adicione arquivos ou pastas que serão ignorados ao buscar por arquivos órfãos. Clique em um caminho para removê-lo da lista." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar ficheiros ou pastas que serão ignorados ao procurar ficheiros órfãos. Clique num caminho para o remover da lista." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Добавьте файлы или папки, которые будут игнорироваться при поиске потерянных файлов. Щелкните на путь, чтобы убрать его из списка." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pridaj súbory alebo priečinky, ktoré sa pri hľadaní osamelých súborov budú ignorovať. Kliknutím na cestu ju odstrániš zo zoznamu." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Dodajte datoteke ali mape, ki bodo prezrte pri iskanju osirotelih datotek. Kliknite pot, da jo odstranite s seznama." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Başıboş dosyaları ararken yok sayılacak dosya veya klasörleri ekleyin. Listeden kaldırmak için bir dizine tıklayın." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Додати файл чи папку які будуть ігноровані при пошуку осиротілих файлів. Клікни на шлях щоб прибрати його зі списку" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thêm các tệp hoặc thư mục sẽ bị bỏ qua khi tìm kiếm tệp dư thừa. Nhấp vào một đường dẫn để xóa khỏi danh sách." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "添加在搜索孤立文件时将被忽略的文件或文件夹。点击路径将其从列表中移除。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "加入搜尋孤立檔案時會忽略的檔案或資料夾。按一下路徑以將其從列表中移除。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "加入搜尋孤立檔案時會忽略的檔案或資料夾。按一下路徑以將其從列表中移除。" } } } }, "Add Filter" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Filter hinzufügen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Agregar filtro" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ajouter un filtre" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tambahkan Filter" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiungi filtro" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "フィルターを追加" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "필터 추가" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj filtr" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar Filtro" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar Filtro" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Добавить фильтр" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pridať filter" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj filter" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Filtre Ekle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Додати фільтр" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thêm Bộ lọc" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "添加筛选器" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "添加篩選器" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "添加篩選器" } } } }, "Add filters and click Play to find files" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Fügen Sie Filter hinzu und klicken Sie auf Abspielen, um Dateien zu finden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Agregue filtros y haga clic en Reproducir para encontrar archivos" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ajoutez des filtres et cliquez sur Lecture pour trouver des fichiers" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tambahkan filter dan klik Mulai untuk menemukan file" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiungi filtri e fai clic su Play per trovare i file" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "フィルターを追加して再生をクリックしてファイルを見つける" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "필터를 추가하고 재생을 클릭하여 파일을 찾으세요" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj filtry i kliknij Odtwórz, aby znaleźć pliki" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Adicione filtros e clique em Reproduzir para encontrar arquivos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Adicione filtros e clique em Reproduzir para encontrar arquivos" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Добавьте фильтры и нажмите Воспроизвести, чтобы найти файлы" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pridajte filtre a kliknite na Prehrať, aby ste našli súbory" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Dodajte filtre in kliknite Predvajaj za iskanje datotek" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Dosyaları bulmak için filtre ekleyin ve Oynat'a tıklayın" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Додайте фільтри та натисніть Відтворити, щоб знайти файли" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thêm bộ lọc và nhấn Phát để tìm tệp" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "添加筛选器并点击播放以查找文件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "添加篩選器並點擊播放以查找文件" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "添加篩選器並點擊播放以查找文件" } } } }, "Add folder" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ordner hinzufügen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Agregar carpeta" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ajouter un dossier" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tambah folder" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiungi cartella" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "フォルダを追加" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "폴더 추가" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj folder" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar pasta" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar pasta" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Добавить папку" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pridať priečinok" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj mapo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Dosya ekle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Додати папку" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thêm thư mục" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "添加文件夹" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "加入資料夾" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "加入資料夾" } } } }, "Add Name Filter" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Namensfilter hinzufügen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Agregar filtro de nombre" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ajouter un filtre de nom" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tambahkan Filter Nama" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiungi Filtro Nome" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "名前フィルターを追加" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이름 필터 추가" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj filtr nazwy" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar Filtro de Nome" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar Filtro de Nome" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Добавить фильтр по имени" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pridať filter mena" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj filter imena" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ad Filtre Ekle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Додати фільтр імен" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thêm Bộ lọc Tên" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "添加名称筛选器" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "添加名稱篩選器" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "添加名稱篩選器" } } } }, "Add Schedule" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Zeitplan hinzufügen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Agregar horario" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ajouter un planning" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tambahkan Jadwal" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiungi programma" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "スケジュールを追加" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "일정 추가" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj harmonogram" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar cronograma" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar Agenda" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Добавить расписание" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pridať plán" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj urnik" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Program Ekle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Додати розклад" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thêm lịch trình" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "添加日程" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "新增行程" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "新增排程" } } } }, "Add Size Filter" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Größenfilter hinzufügen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Agregar filtro de tamaño" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ajouter un filtre de taille" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tambahkan filter ukuran" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiungi Filtro Dimensione" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "サイズフィルターを追加" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "크기 필터 추가" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj filtr rozmiaru" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar filtro de tamanho" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar Filtro de Tamanho" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Добавить фильтр размера" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pridať filter veľkosti" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj filter velikosti" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Boyut filtresi ekle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Додати фільтр за розміром" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thêm bộ lọc kích thước" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "添加大小筛选器" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "添加大小篩選器" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "添加大小篩選器" } } } }, "Add Tags Filter" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Tags-Filter hinzufügen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Agregar filtro de etiquetas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ajouter un filtre de tags" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tambahkan Filter Tag" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiungi filtro tag" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "タグフィルターを追加" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "태그 필터 추가" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj filtr tagów" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar Filtro de Tags" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar Filtro de Etiquetas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Добавить фильтр тегов" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pridať filter značiek" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj filter oznak" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Etiket Filtresi Ekle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Додати фільтр тегів" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thêm bộ lọc thẻ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "新增标签筛选器" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "新增標籤篩選器" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "新增標籤篩選器" } } } }, "Add Tap" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Hahn hinzufügen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Agregar grifo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ajouter un robinet" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tambahkan Keran" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiungi Tap" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "タップを追加" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "탭 추가" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj kran" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar Torneira" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Adicionar Tap" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Добавить кран" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pridať kohútik" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Dodaj Tap" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Musluk Ekle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Додати кран" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thêm Vòi" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "新增水龙头" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "新增水龍頭" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "新增水龍頭" } } } }, "Adding..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Hinzufügen..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Añadiendo..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ajout..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Menambahkan..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiunta in corso..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "追加中..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "추가 중..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dodawanie..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Adicionando..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A adicionar..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Добавление..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pridávanie..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Dodajanje..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ekleniyor..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Додавання..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang thêm..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "新增中..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "新增中..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "新增中..." } } } }, "Additional Info:" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Zusätzliche Informationen:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Información Adicional:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Informations supplémentaires:" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Informasi Tambahan:" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Informazioni aggiuntive:" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "追加情報:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "추가 정보:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dodatkowe informacje:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Informações Adicionais:" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Informação Adicional:" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Дополнительная информация:" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Ďalšie informácie:" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Dodatne informacije:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ek Bilgi:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Додаткова інформація:" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thông tin bổ sung:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "附加信息:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "附加信息:" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "附加信息:" } } } }, "Adopt" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Adoptieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Adoptar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Adopter" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Mengadopsi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Adottare" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "採用" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "채택" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Adoptować" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Adotar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Adotar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Принять" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Adoptovať" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Posvojiti" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Benimse" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Усиновити" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Nhận nuôi" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "采纳" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "採納" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "採納" } } } }, "Adopt with Homebrew" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Übernehmen mit Homebrew" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Adoptar con Homebrew" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Adopter avec Homebrew" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Adopsi dengan Homebrew" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Adotta con Homebrew" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Homebrewで採用" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew에서 가져옴" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zaadoptuj z Homebrew" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Adotar com Homebrew" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Adotar com Homebrew" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Принять с Homebrew" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Prijať s Homebrew" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Sprejmi s Homebrew" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew ile benimse" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Прийняти з Homebrew" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chấp nhận với Homebrew" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "采用 Homebrew" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "採用 Homebrew" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "採用 Homebrew" } } } }, "Adopting..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Adoptieren..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Adoptando..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Adoption..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Mengadopsi..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Adottando..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "採用中..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "가져오는 중..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Przyjmowanie..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Adotando..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A adotar..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Принятие..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Adopcia..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Posvojitev..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Benimseniyor..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Прийняття..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang áp dụng..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "采用中..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "採用中..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "採用中..." } } } }, "All checkboxes" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Alle Checkboxen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Todas las casillas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Toutes les cases à cocher" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Semua kotak centang" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Tutte le caselle di controllo" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "すべてのチェックボックス" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "모든 체크박스" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wszystkie pola wyboru" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Todas as caixas de seleção" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Todas as caixas de seleção" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Все флажки" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Všetky zaškrtávacie políčka" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Vsa potrditvena polja" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Tüm kutucuklar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Всі чекбокси" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tất cả hộp chọn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "所有复选框" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "全部剔選框" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "全部剔選框" } } } }, "All updates are hidden" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Alle Updates sind ausgeblendet" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Todas las actualizaciones están ocultas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Toutes les mises à jour sont masquées" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Semua pembaruan disembunyikan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Tutti gli aggiornamenti sono nascosti" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "すべての更新が非表示になっています" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "모든 업데이트가 숨겨져 있습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wszystkie aktualizacje są ukryte" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Todas as atualizações estão ocultas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Todas as atualizações estão ocultas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Все обновления скрыты" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Všetky aktualizácie sú skryté" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Vse posodobitve so skrite" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Tüm güncellemeler gizlendi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Усі оновлення приховані" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tất cả các cập nhật đều bị ẩn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "所有更新都被隐藏" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "所有更新都被隱藏" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "所有更新都被隱藏" } } } }, "All valid package files from the BOM list have been removed. You may Forget this package." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Alle gültigen Paketdateien aus der BOM-Liste wurden entfernt. Sie können dieses Paket vergessen." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Todos los archivos de paquetes válidos de la lista BOM han sido eliminados. Puede olvidar este paquete." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Tous les fichiers de package valides de la liste BOM ont été supprimés. Vous pouvez oublier ce package." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Semua file paket yang valid dari daftar BOM telah dihapus. Anda dapat Lupakan paket ini." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Tutti i file di pacchetto validi dalla lista BOM sono stati rimossi. Puoi dimenticare questo pacchetto." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "BOMリストからすべての有効なパッケージファイルが削除されました。このパッケージを忘れられます。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "BOM 목록의 모든 유효한 패키지 파일이 제거되었습니다. 이 패키지를 잊을 수 있습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wszystkie ważne pliki pakietów z listy BOM zostały usunięte. Możesz zapomnieć o tym pakiecie." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Todos os arquivos de pacote válidos da lista BOM foram removidos. Você pode esquecer este pacote." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Todos os ficheiros de pacotes válidos da lista BOM foram removidos. Pode esquecer este pacote." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Все допустимые файлы пакета из списка BOM были удалены. Вы можете забыть этот пакет." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Všetky platné súbory balíkov zo zoznamu BOM boli odstránené. Môžete na tento balík zabudnúť." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Vse veljavne paketne datoteke s seznama BOM so bile odstranjene. Lahko pozabite ta paket." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "BOM listesindeki tüm geçerli paket dosyaları kaldırıldı. Bu paketi Unutabilirsiniz." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Усі дійсні файли пакунків зі списку BOM було видалено. Ви можете Забути цей пакунок." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tất cả các tệp gói hợp lệ từ danh sách BOM đã được xóa. Bạn có thể quên gói này." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "BOM列表中的所有有效包文件已被移除。您可以遗忘此包。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "來自BOM清單的所有有效套件文件已被移除。您可以忘記此套件" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "BOM 清單中所有有效的軟件包文件已被移除。您可以忘記此軟件包。" } } } }, "Also in: %@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Auch in: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "También en: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Aussi dans: %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Juga di: %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Anche in: %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "さらに: %@ でも" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "또한 포함: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Również w: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Também em: %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Também em: %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Также в: %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Tiež v: %@\"" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Tudi v: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ayrıca şurada: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Також у: %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Cũng nằm trong: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "也在:%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "同時在:%@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "同樣位於:%@" } } } }, "Always confirm the files marked for removal. In rare cases, unrelated files may be found when app names are too similar." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Bestätigen Sie immer die markierten Dateien zum löschen. In seltenen Fällen können Dateien gefunden werden, die nichts mit der App zu tun haben, wenn die App-Namen zu ähnlich sind." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Siempre confirme los archivos marcados para su eliminación. En casos raros, se pueden encontrar archivos no relacionados cuando los nombres de las aplicaciones son demasiado similares." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Vérifiez toujours les fichiers marqués pour suppression. Dans de rares cas, des fichiers sans rapport peuvent être détectés lorsque les noms d'applications sont trop similaires." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Selalu konfirmasi file yang ditandai untuk dihapus. Dalam kasus yang jarang terjadi, file yang tidak terkait mungkin ditemukan ketika nama aplikasi terlalu mirip." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Conferma sempre i file contrassegnati per la rimozione. In rari casi, possono essere trovati file non correlati quando i nomi delle app sono troppo simili." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "削除対象としてマークされたファイルを常に確認してください。まれに、アプリ名が似すぎている場合に関連のないファイルが検出されることがあります。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "제거할 파일을 항상 확인하세요. 드물게 앱 이름이 너무 비슷한 경우 관련 없는 파일이 발견될 수 있습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zawsze potwierdzaj pliki zaznaczone do usunięcia. W rzadkich przypadkach mogą zostać znalezione niepowiązane pliki, gdy nazwy aplikacji są zbyt podobne." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Sempre confirme os arquivos marcados para remoção. Em casos raros, arquivos não relacionados podem ser encontrados quando os nomes dos aplicativos são muito semelhantes." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Confirme sempre os ficheiros marcados para remoção. Em casos raros, ficheiros não relacionados podem ser encontrados quando os nomes das aplicações são muito semelhantes." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Всегда подтверждайте файлы, отмеченные для удаления. В редких случаях могут быть найдены несвязанные файлы, если названия приложений слишком похожи." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vždy potvrďte súbory označené na odstránenie. V zriedkavých prípadoch môžu byť nájdené nesúvisiace súbory, keď sú názvy aplikácií príliš podobné." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Vedno potrdite datoteke, označene za odstranitev. V redkih primerih se lahko pojavijo nepovezane datoteke, kadar so imena aplikacij preveč podobna." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kaldırılmak üzere işaretlenen dosyaları her zaman kontrol edin. Nadir durumlarda, uygulama adları birbirine çok benzer ise alakasız dosyalar bulunabilir." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Завжди перевіряйте файли, позначені для видалення. У рідкісних випадках можуть бути знайдені не пов'язані файли, якщо назви програм занадто схожі." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Luôn xác nhận các tệp được đánh dấu để xóa. Trong một số trường hợp hiếm hoi, có thể tìm thấy các tệp không liên quan khi tên ứng dụng quá giống nhau." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "始终确认标记为删除的文件。在极少数情况下,当应用名称过于相似时,可能会找到无关的文件。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "總是確認標記為移除的檔案。在少數個案中,當應用程式名稱太相似時,可能會找到不相關的檔案。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "總是確認標記為移除的檔案。在少數個案中,當應用程式名稱太相似時,可能會找到不相關的檔案。" } } } }, "Always confirm the files marked for removal. In rare cases, unrelated files may be found when app names are too similar.\n\nNOTE: Pearcleaner uses AppleScript to remove files. Currently macOS does not allow AppleScript to authenticate using the fingerprint sensor, only password authentication is supported." : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Bestätigen Sie immer die zum Entfernen markierten Dateien. In seltenen Fällen können nicht verwandte Dateien gefunden werden, wenn die Namen der Anwendungen zu ähnlich sind.\n\nHINWEIS: Pearcleaner verwendet AppleScript, um Dateien zu entfernen. Derzeit erlaubt macOS nicht, dass AppleScript sich mit dem Fingerabdrucksensor authentifiziert, nur die Passwortauthentifizierung wird unterstützt." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Siempre confirme los archivos marcados para su eliminación. En casos raros, se pueden encontrar archivos no relacionados cuando los nombres de las aplicaciones son demasiado similares\n\nNOTA: Pearcleaner utiliza AppleScript para eliminar archivos. Actualmente macOS no permite que AppleScript se autentique usando el sensor de huellas dactilares, solo se admite la autenticación con contraseña" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Demande toujours de confirmer avant la suppression de potentiel fichiers. Dans de rares cas, des fichiers sans rapport peuvent être trouvés lorsque des noms d'applications sont trop similaires.\n\nREMARQUE : Pearcleaner utilise AppleScript pour supprimer les fichiers trouvés et comme macOS ne permet pas à AppleScript de s'authentifier à l'aide du capteur d'empreinte digitale, seule l'authentification par mot de passe est prise en charge." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Selalu konfirmasi file yang ditandai untuk dihapus. Dalam kasus yang jarang, file yang tidak terkait mungkin ditemukan ketika nama aplikasi terlalu mirip.\n\nCATATAN: Pearcleaner menggunakan AppleScript untuk menghapus file. Saat ini macOS tidak mengizinkan AppleScript untuk mengautentikasi menggunakan sensor sidik jari, hanya mendukung autentikasi melalui kata sandi." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Conferma sempre i file contrassegnati per la rimozione. In rari casi, possono essere trovati file non correlati quando i nomi delle app sono troppo simili.\n\nNOTA: Pearcleaner utilizza AppleScript per rimuovere i file. Attualmente macOS non consente ad AppleScript di autenticarsi utilizzando il sensore di impronte digitali, è supportata solo l'autenticazione tramite password." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "削除対象としてマークされたファイルを常に確認してください。稀に、アプリ名が似すぎる場合、関係のないファイルが見つかることがあります。\\n\\n注意: Pearcleaner はファイルを削除するために AppleScript を使用します。現在、macOS では AppleScript が指紋センサーを使用して認証することはできません。パスワード認証のみがサポートされています。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "제거 표시된 파일을 항상 확인하세요. 드물게 앱 이름이 너무 비슷할 경우 관련 없는 파일이 발견될 수 있습니다.\n\n참고: Pearcleaner는 AppleScript를 사용하여 파일을 제거합니다. 현재 macOS는 Touch ID를 사용한 인증을 허용하지 않으며, 비밀번호 인증만 지원됩니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zawsze potwierdzaj pliki zaznaczone do usunięcia. W rzadkich przypadkach mogą zostać znalezione niepowiązane pliki, gdy nazwy aplikacji są zbyt podobne.\n\nUWAGA: Pearcleaner używa AppleScript do usuwania plików. Obecnie system macOS nie pozwala AppleScript na uwierzytelnianie za pomocą czytnika linii papilarnych, obsługiwane jest tylko uwierzytelnianie hasłem." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Sempre confirme os arquivos marcados para remoção. Em casos raros, arquivos não relacionados podem ser encontrados quando os nomes dos aplicativos são muito semelhantes\n\nNOTA: O Pearcleaner usa AppleScript para remover arquivos. Atualmente o macOS não permite que o AppleScript autentique usando o sensor de impressão digital, apenas a autenticação por senha é suportada" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Confirme sempre os ficheiros marcados para remoção. Em casos raros, ficheiros não relacionados podem ser encontrados quando os nomes das aplicações são muito semelhantes.\n\nNOTA: O Pearcleaner utiliza AppleScript para remover ficheiros. Atualmente, o macOS não permite que o AppleScript autentique usando o sensor de impressões digitais, apenas a autenticação por palavra-passe é suportada." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Всегда проверяйте файлы, отмеченные для удаления. В редких случаях могут быть найдены посторонние файлы, если названия приложений слишком похожи.\n\nПРИМЕЧАНИЕ: Pearcleaner использует AppleScript для удаления файлов. В настоящее время macOS не позволяет AppleScript проходить аутентификацию с помощью отпечатка пальцев, поддерживается только пароль." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vždy potvrď súbory označené na odstránenie. V zriedkavých prípadoch sa môžu nájsť nesúvisiace súbory, keď sú názvy aplikácií príliš podobné.\n\nPOZNÁMKA: Pearcleaner používa na odstránenie súborov AppleScript. V súčasnosti macOS neumožňuje AppleScriptu autentifikovať pomocou snímača odtlačkov prstov, podporované je iba overenie heslom." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Vedno potrdite datoteke, označene za odstranitev. V redkih primerih se lahko najdejo nepovezane datoteke, ko so imena aplikacij preveč podobna.\n\nOPOMBA: Pearcleaner uporablja AppleScript za odstranjevanje datotek. Trenutno macOS ne omogoča, da bi AppleScript preverjal pristnost z uporabo senzorja prstnih odtisov, podprta je le avtentikacija z geslom." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kaldırmak üzere işaretlenmiş dosyaları her zaman kontrol edin. Nadir durumlarda, uygulama adları birbirine çok benzer ise alakasız dosyalar bulunabilir.\n\nNOT: Pearcleaner dosyaları kaldırmak için AppleScript kullanır. Şimdilik macOS, AppleScript’in parmak izi sensörü ile doğrulama yapmasına izin vermiyor, sadece şifre ile doğrulama mümkün." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Завжди перевіряйте файли, позначені для видалення. У рідкісних випадках можуть бути знайдені не пов'язані файли, якщо назви програм занадто схожі.\n\nПРИМІТКА: Pearcleaner використовує AppleScript для видалення файлів. Наразі macOS не дозволяє AppleScript автентифікуватися за допомогою сканера відбитків пальців, підтримується лише автентифікація за допомогою пароля." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Luôn xác nhận các tệp được đánh dấu để xóa. Trong một số trường hợp hiếm hoi, có thể tìm thấy các tệp không liên quan khi tên ứng dụng quá giống nhau.\n\nLưu ý: Pearcleaner sử dụng AppleScript để xóa tệp tin. Hiện tại macOS không cho phép AppleScript xác thực bằng cảm biến vân tay, chỉ hỗ trợ xác thực bằng mật khẩu." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "请始终确认标记为删除的文件。某些情况下,当应用名称过于相似时,可能会找到无关的文件。\n\n注意:Pearcleaner 使用 AppleScript 删除文件。目前 macOS 不允许 AppleScript 使用指纹传感器进行身份验证,仅支持密码验证。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "總是確認標記為移除的檔案。在少數個案中,當應用程式名稱太相似時,可能會找到不相關的檔案。\n\n注意:Pearcleaner 使用 AppleScript 來移除檔案。現時 macOS 不允許 AppleScript 使用指紋感應器進行身分認證,只支援密碼身分認證。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "總是確認標記為移除的檔案。在少數個案中,當應用程式名稱太相似時,可能會找到不相關的檔案。\n\n注意:Pearcleaner 使用 AppleScript 來移除檔案。現時 macOS 不允許 AppleScript 使用指紋感應器進行身分認證,只支援密碼身分認證。" } } } }, "Analytics" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Analyse" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Analítica" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Analytique" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Analisis" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Analisi" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "分析" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "분석" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Analiza" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Análise" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Análise" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Аналитика" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Analytika" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Analitika" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Analitik" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Аналітика" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Phân tích" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "分析" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "分析" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "分析" } } } }, "Analytics are disabled" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Analysen sind deaktiviert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Los análisis están desactivados" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Les analyses sont désactivées" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Analitik dinonaktifkan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Le analisi sono disabilitate" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "分析は無効です" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "분석이 비활성화되었습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Analiza jest wyłączona" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Análises estão desativadas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Análises estão desativadas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Аналитика отключена" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Analytika je vypnutá" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Analitika je onemogočena" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Analizler devre dışı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Аналітика вимкнена" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Phân tích bị vô hiệu hóa" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "分析已停用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "分析已停用" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "分析已停用" } } } }, "Analytics are enabled" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Analysen sind aktiviert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Los análisis están habilitados" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Les analyses sont activées" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Analitik diaktifkan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Le analisi sono abilitate" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "分析が有効になっています" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "분석이 활성화되었습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Analiza jest włączona" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Análises estão ativadas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Análises estão ativadas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Аналитика включена" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Analytika je povolená" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Analitika je omogočena" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Analizler etkinleştirildi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Аналітика увімкнена" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Phân tích được bật" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "分析已启用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "分析已啟用" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "分析已啟用" } } } }, "Animations disabled" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Animationen deaktiviert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Animaciones desactivadas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Animations de l’application" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Animasi dinonaktifkan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Animazioni disabilitate" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アニメーションを無効にする" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "애니메이션 비활성화" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Animacje wyłączone" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Animações desativadas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Animações desativadas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Анимации отключены" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Animácie vypnuté" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Animacije onemogočene" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Animasyonlar devre dışı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Анімації вимкнено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đã tắt hiển thị hoạt ảnh" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "动画已禁用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "停用動畫" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "停用動畫" } } } }, "Animations enabled" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Animationen aktiviert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Animaciones habilitadas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Animations de l’application" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Animasi diaktifkan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Animazioni abilitate" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アニメーションを有効にする" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "애니메이션 활성화" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Animacje włączone" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Animações ativadas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Animações ativadas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Анимации включены" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Animácie zapnuté" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Animacije omogočene" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Animasyonlar etkin" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Анімації увімкнено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đã bật hiển thị hoạt ảnh" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "动画已启用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "啟用動畫" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "啟用動畫" } } } }, "Announcement" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ankündigung" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Anuncio" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Annonces" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pengumuman" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Annuncio" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "お知らせ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "공지" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ogłoszenie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Anúncio" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Anúncio" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Объявление" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Oznámenie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Obvestilo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Duyuru" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оголошення" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tuyên bố" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "公告" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "宣佈" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "宣佈" } } } }, "App is up to date" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "App ist auf dem neuesten Stand" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "La aplicación está actualizada" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "L'application est à jour" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Aplikasi sudah diperbarui" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "L'app è aggiornata" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アプリは最新です" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "앱이 최신 상태입니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Aplikacja jest aktualna" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "O aplicativo está atualizado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A aplicação está atualizada" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Приложение обновлено" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Aplikácia je aktuálna" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Aplikacija je posodobljena" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uygulama güncel" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Додаток оновлено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ứng dụng đã được cập nhật" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "应用是最新的" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "應用程式已是最新" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "應用程式已是最新" } } } }, "App Lipo" : { "comment" : "Lipo alert title", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "App Lipo" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Alerta Lipo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "App Lipo" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Peringatan Lipo" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "App Lipo" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アプリリポ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "앱 번들 축소" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Lipo aplikacji" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Alerta do Lipo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "App Lipo" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Липо приложений" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Aplikácia Lipo" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "App Lipo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uygulama Lipo’su" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Програма Lipo" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Cảnh báo Lipo" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "App 瘦身" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "App 節省" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "App 節省" } } } }, "App Name" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Programmname" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Nombre de la aplicación" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Nom de l’application" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Nama Aplikasi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nome dell'app" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アプリ名" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "앱 이름" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nazwa aplikacji" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nome do App" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Nome da App" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Название приложения" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Názov aplikácie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ime aplikacije" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uygulama Adı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Назва програми" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tên ứng dụng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "应用名" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "App 名稱" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "App 名稱" } } } }, "App not found" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "App nicht gefunden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Aplicación no encontrada" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Application introuvable" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Aplikasi tidak ditemukan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "App non trovata" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アプリが見つかりません" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "앱을 찾을 수 없습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Aplikacja nie została znaleziona" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Aplicativo não encontrado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Aplicação não encontrada" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Приложение не найдено" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Aplikácia nebola nájdená" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Aplikacija ni bila najdena" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uygulama bulunamadı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Додаток не знайдено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không tìm thấy ứng dụng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "找不到应用程序" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "找不到應用程式" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "找不到應用程式" } } } }, "App Size" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Programmgrösse" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Tamaño de la aplicación" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Taille de l’application" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ukuran Aplikasi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Dimensione app" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アプリのサイズ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "앱 크기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Rozmiar aplikacji" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Tamanho do App" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Tamanho da Aplicação" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Размер приложения" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Veľkosť aplikácie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Velikost aplikacije" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uygulama Boyutu" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Розмір програми" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Dung lượng ứng dụng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "应用大小" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "App 大小" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "App 大小" } } } }, "App Store" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "App Store" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "App Store" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "App Store" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "App Store" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "App Store" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "App Store" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "App Store" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "App Store" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "App Store" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "App Store" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "App Store" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "App Store" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "App Store" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "App Store" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "App Store" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "App Store" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "App Store" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "App Store" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "App Store" } } } }, "App Store App Detected" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "App Store App erkannt" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Aplicación de App Store detectada" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Application App Store détectée" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Aplikasi App Store Terdeteksi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "App Store App rilevata" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "App Storeアプリが検出されました" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "앱 스토어 앱이 감지되었습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wykryto aplikację App Store" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Aplicativo da App Store Detectado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Aplicação da App Store Detetada" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Обнаружено приложение App Store" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zistená aplikácia App Store" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Aplikacija App Store zaznana" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "App Store Uygulaması Tespit Edildi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Виявлено додаток App Store" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đã phát hiện ứng dụng App Store" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "检测到 App Store 应用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "偵測到 App Store 應用程式" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "偵測到 App Store 應用程式" } } } }, "Appcast URL" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Appcast-URL" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "URL de Appcast" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "URL de l'Appcast" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "URL Appcast" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "URL Appcast" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Appcast URL" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Appcast URL" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "URL Appcast" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "URL do Appcast" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "URL do Appcast" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "URL Appcast" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "URL Appcast" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "URL za Appcast" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Appcast URL" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "URL Appcast" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "URL Appcast" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Appcast URL" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Appcast URL" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "Appcast URL" } } } }, "Appearance" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Erscheinungsbild" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Apariencia" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Apparence" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tampilan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aspetto" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "外観" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "외관" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wygląd" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Aparência" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Aparência" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Внешний вид" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vzhľad" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Videz" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Görünüm" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оформлення" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Giao diện" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "外观" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "外觀" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "外觀" } } } }, "Application file is nested within subdirectories. To prevent deleting incorrect folders, Pearcleaner will leave these alone. You may manually delete the remaining folders if required." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Die Programmdatei ist in Unterverzeichnissen verschachtelt. Um zu verhindern, dass falsche Ordner gelöscht werden, lässt Pearcleaner diese aus. Sie können die übrigen Ordner bei Bedarf manuell löschen." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "El archivo de la aplicación está anidado dentro de subdirectorios. Para evitar eliminar carpetas incorrectas, Pearcleaner las dejará en paz. Puede eliminar manualmente las carpetas restantes si es necesario." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Le fichier d'application est imbriqué dans des sous-répertoires. Pour éviter de supprimer des dossiers incorrects, Pearcleaner les laissera seuls. Vous pourrez supprimer les dossiers restants si nécessaire manuellement." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "File aplikasi berada dalam subdirektori. Untuk mencegah kesalahan penghapusan folder, Pearcleaner akan mengabaikannya. Anda dapat menghapus folder yang tersisa secara mandiri jika diperlukan." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Il file dell'applicazione è annidato all'interno di sottodirectory. Per evitare di eliminare cartelle errate, Pearcleaner lascerà queste da sole. Puoi eliminare manualmente le cartelle rimanenti se necessario." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アプリケーションファイルはサブディレクトリに入れ子になっています。間違ったフォルダを削除しないように、Pearcleaner はこれらをそのままにします。必要に応じて、残りのフォルダを手動で削除することができます。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "응용 프로그램 파일이 하위 디렉토리에 포함되어 있습니다. 잘못된 폴더 삭제를 방지하기 위해 Pearcleaner는 이를 그대로 둡니다. 필요한 경우 남은 폴더를 수동으로 삭제할 수 있습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Plik aplikacji jest zagnieżdżony w podfolderach. Aby zapobiec usunięciu nieprawidłowych folderów, Pearcleaner zostawi je. W razie potrzeby można ręcznie usunąć pozostałe foldery." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "O arquivo do aplicativo está aninhado dentro de subdiretórios. Para evitar a exclusão de pastas incorretas, o Pearcleaner deixará essas pastas como estão. Você pode excluir manualmente as pastas restantes, se necessário." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "O ficheiro da aplicação está aninhado dentro de subdiretórios. Para evitar a eliminação de pastas incorretas, o Pearcleaner deixará estas em paz. Pode eliminar manualmente as pastas restantes, se necessário." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Файл приложения находится в подпапках. Чтобы избежать удаления неправильных папок, Pearcleaner оставит их без изменений. При необходимости вы можете вручную удалить оставшиеся папки." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Súbor aplikácie je vnorený do podadresárov. Aby sa predišlo odstráneniu nesprávnych priečinkov, Pearcleaner ich nechá tak. V prípade potreby môžeš zostávajúce priečinky manuálne odstrániť." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Datoteka aplikacije je ugnezdena v podimenikih. Da preprečite brisanje napačnih map, jih bo Pearcleaner pustil pri miru. Preostale mape lahko po potrebi izbrišete ročno." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uygulama dosyası alt dizinler içinde bulunuyor. Yanlış dosyaların silinmesini önlemek için Pearcleaner bunlara müdahale etmeyecek. Gerekirse kalan klasörleri manuel olarak silebilirsiniz." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Файл програми вкладено у підпапки. Щоб запобігти видаленню неправильних папок, Pearcleaner залишить їх без змін. За потреби ви можете вручну видалити решту папок." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tệp ứng dụng nằm trong các thư mục con. Để tránh xóa nhầm các thư mục, Pearcleaner sẽ bỏ qua chúng. Bạn có thể xóa các thư mục còn lại thủ công nếu cần." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "应用程序文件嵌套在子目录中。为了防止删除错误的文件夹,Pearcleaner 将保留这些文件夹。如果需要,您可以手动删除剩余的文件夹。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "應用程式檔案包藏在子目錄中。為了避免刪除不正確的資料夾,Pearcleaner 會保留這些。如有要求,你可以手動刪除剩餘的資料夾。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "應用程式檔案包藏在子目錄中。為了避免刪除不正確的資料夾,Pearcleaner 會保留這些。如有要求,你可以手動刪除剩餘的資料夾。" } } } }, "Application Folder" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Anwendungsordner" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Carpeta de Aplicaciones" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Dossier d'application" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Folder Aplikasi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Cartella Applicazioni" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アプリケーションフォルダ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "애플리케이션 폴더" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Folder aplikacji" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Pasta de Aplicativos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Pasta de Aplicações" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Папка приложения" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Priečinok aplikácie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Mapa aplikacij" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uygulama Klasörü" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Папка додатка" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thư mục ứng dụng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "应用程序文件夹" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "應用程式資料夾" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "應用程式資料夾" } } } }, "Application Script" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Programm Skript" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Script de la Aplicación" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Script d’application" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Skrip Aplikasi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Script dell'applicazione" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アプリスクリプト" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "애플리케이션 스크립트" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Skrypt aplikacji" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Script de Aplicativo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Script de Aplicação" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Скрипт приложения" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Skript aplikácie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Skript aplikacije" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uygulama Betiği" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Скрипт програми" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Kịch bản ứng dụng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "应用脚本" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "App 程式碼" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "App 程式碼" } } } }, "Applications" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Programme" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Aplicaciones" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Applications" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Aplikasi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Applicazioni" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アプリケーション" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "애플리케이션" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Aplikacje" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Aplicativos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Aplicações" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Приложения" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Aplikácie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Aplikacije" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uygulamalar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Програми" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ứng dụng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "应用程序" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "應用程式" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "應用程式" } } } }, "Approximate Savings" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ungefähre Einsparungen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ahorros aproximados" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Économies approximatives" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Perkiraan Penghematan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Risparmio approssimativo" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "概算節約額" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "대략적인 절약" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Przybliżone oszczędności" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Economia Aproximada" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Economia Aproximada" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Примерная экономия" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Približné úspory" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Približni prihranki" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yaklaşık Tasarruf" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Приблизна економія" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tiết kiệm ước tính" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "大致节省" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "大約節省" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "大約節省" } } } }, "Approximate Savings:" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ungefähre Einsparungen:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ahorros aproximados:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Économies approximatives :" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Perkiraan Penghematan:" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Risparmio approssimativo:" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "概算節約額:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "대략적인 절약: " } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Przybliżone oszczędności:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Economia Aproximada:" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Poupança Aproximada:" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Примерная экономия:" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Približné úspory:" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Približni prihranki:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yaklaşık Tasarruf:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Приблизна економія:" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tiết kiệm ước tính:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "大致节省:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "約計儲蓄:" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "大約儲蓄:" } } } }, "Apps" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Apps" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Aplicaciones" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Apps" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Aplikasi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "App" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アプリケーション" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "앱" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Aplikacje" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Aplicativos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Aplicativos" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Приложения" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Aplikácie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Aplikacije" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uygulamalar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Програми" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ứng dụng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "应用程序" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "應用程式" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "應用程式" } } } }, "Architecture: %@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Architektur: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Arquitectura: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Architecture : %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Arsitektur: %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Architettura: %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アーキテクチャ: %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "아키텍처: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Architektura: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Arquitetura: %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Arquitetura: %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Архитектура: %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Architektúra: %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Arhitektura: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Mimari: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Архітектура: %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Kiến trúc: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "架构: %@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "架構: %@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "架構: %@" } } } }, "Are you sure you want to remove these files?" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Sind Sie sicher, dass Sie diese Dateien entfernen möchten?" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "¿Estás seguro de que deseas eliminar estos archivos?" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Êtes-vous sûr(e) de vouloir supprimer ces fichiers ?" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Apakah Anda yakin ingin menghapus file-file ini?" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Sei sicuro di voler rimuovere questi file?" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "これらのファイルを削除してもよろしいですか?" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 파일들을 제거하시겠습니까?" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Czy na pewno chcesz usunąć te pliki?" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Tem certeza de que deseja remover esses arquivos?" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Tem a certeza de que deseja remover estes ficheiros?" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Вы уверены, что хотите удалить эти файлы?" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Naozaj chceš tieto súbory odstrániť?" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ali ste prepričani, da želite odstraniti te datoteke?" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu dosyaları kaldırmak istediğinize emin misiniz?" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ви впевнені, що хочете видалити ці файли?" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Bạn có chắc chắn sẽ xóa những tập tin sau đây không?" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "您是否确认想要删除这些文件?" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "確定要移除這些檔案嗎?" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "確定要移除這些檔案嗎?" } } } }, "Are you sure you want to remove this tap?" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Sind Sie sicher, dass Sie diesen Tab entfernen möchten?" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "¿Está seguro de que desea eliminar esta pestaña?" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Êtes-vous sûr de vouloir supprimer cet onglet?" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Apakah Anda yakin ingin menghapus keran ini?" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Sei sicuro di voler rimuovere questo tap?" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "このタブを削除してもよろしいですか?" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 탭을 제거하시겠습니까?" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Czy na pewno chcesz usunąć tę kartę?" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Tem certeza de que deseja remover esta aba?" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Tem a certeza de que deseja remover este tap?" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Вы уверены, что хотите удалить эту вкладку?" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Ste si istý, že chcete odstrániť túto kartu?" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ali ste prepričani, da želite odstraniti to pipo?" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu sekmeyi kaldırmak istediğinizden emin misiniz?" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ви впевнені, що хочете видалити цю вкладку?" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Bạn có chắc chắn muốn xóa tab này không?" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "您确定要移除此标签页吗?" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "您確定要移除此標籤頁嗎?" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "您確定要移除此標籤頁嗎?" } } } }, "Artifacts" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Artefakte" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Artefactos" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Artefacts" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Artefak" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Artefatti" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アーティファクト" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "아티팩트" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Artefakty" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Artefatos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Artefactos" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Артефакты" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Artefakty" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Artefakti" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Eserler" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Артефакти" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tạo tác" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "工件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "工件" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "工件" } } } }, "Authorization required in Settings > Login Items > %@.app." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "In den Einstellungen unter Anmeldeobjekte > %@.app ist eine Autorisierung erforderlich" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Autorización requerida en Configuración > Elementos de inicio de sesión > %@.app." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Autorisation requise dans Paramètres > Éléments de connexion > %@.app." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Diperlukan otorisasi di Pengaturan > Item Masuk > %@.app." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Autorizzazione richiesta in Impostazioni > Elementi di login > %@.app." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "設定 > ログイン項目 > %@.app で承認が必要です" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "설정 > 로그인 항목 > %@.app 의 권한이 필요합니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wymagane upoważnienie w Ustawienia systemowe > Rzeczy i rozszerzenia otwierane podczas logowania > %@.app." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Autorização necessária em Ajustes > Itens de Login > %@.app." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Autorização necessária em Definições > Itens de Início de Sessão > %@.app." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Требуется авторизация в Настройки > Объекты входа > %@.app." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vyžaduje sa autorizácia v Nastavenia > Prihlasovanie > %@.app." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Potrebna je avtorizacija v Nastavitve > Prijavni elementi > %@.app." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ayarlar > Oturum Açma Öğeleri > %@.app'de yetkilendirme gerekiyor." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Необхідно авторизувати в Налаштуваннях > Елементи входу > %@.app." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Cần cấp quyền trong Cài đặt > Mục đăng nhập > %@.app." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "需要授权 设置 > 登录项 > %@.app " } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "需要在「設定」>「登入項目」>「%@.app」中授權。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "需要在「設定」>「登入項目」>「%@.app」中授權。" } } } }, "Auto Prune (Keep macOS Language)" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Automatisches Beschneiden (macOS-Sprache beibehalten)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Poda automática (mantener idioma de macOS)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Élagage automatique (Conserver la langue macOS)" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pemangkasan Otomatis (Pertahankan Bahasa macOS)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Potatura automatica (mantieni lingua macOS)" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "自動プルーン (macOS言語を保持)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "자동 제거 (macOS 언어 유지)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Automatyczne przycinanie (Zachowaj język macOS)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Poda Automática (Manter Idioma do macOS)" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Poda Automática (Manter Idioma do macOS)" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Автоочистка (сохранить язык macOS)" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Automatické prerezávanie (Zachovať jazyk macOS)" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Samodejno obrezovanje (Ohrani jezik macOS)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Otomatik Budama (macOS Dilini Koru)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Автоматична обрізка (Зберегти мову macOS)" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tự động tỉa (Giữ ngôn ngữ macOS)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "自动修剪(保留 macOS 语言)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "自動修剪(保留 macOS 語言)" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "自動修剪(保留 macOS 語言)" } } } }, "Automatic updates are disabled" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Automatische Updates sind deaktiviert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Las actualizaciones automáticas están desactivadas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Les mises à jour automatiques sont désactivées" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pembaruan otomatis dinonaktifkan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Gli aggiornamenti automatici sono disabilitati" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "自動更新が無効になっています" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "자동 업데이트가 비활성화되었습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Automatyczne aktualizacje są wyłączone" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualizações automáticas estão desativadas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Atualizações automáticas estão desativadas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Автоматические обновления отключены" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Automatické aktualizácie sú zakázané" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Samodejne posodobitve so onemogočene" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Otomatik güncellemeler devre dışı bırakıldı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Автоматичні оновлення вимкнено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Cập nhật tự động đã bị vô hiệu hóa" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "自动更新已禁用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "自動更新已停用" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "自動更新已停用" } } } }, "Automation" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Automatisierung" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Automatización" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Automatisation" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Otomatisasi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Automazione" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "自動化" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "자동화" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Automatyzacja" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Automação" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Automação" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Автоматизация" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Automatizácia" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Avtomatizacija" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Otomasyon" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Автоматизація" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tự động hóa" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "自动化" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "自動化" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "自動化" } } } }, "Available:" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Verfügbar:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Disponible:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Disponible :" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tersedia:" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Disponibile:" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "利用可能:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "사용 가능:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dostępne:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Disponível:" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Disponível:" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Доступно:" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Dostupné:" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Na voljo:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Mevcut:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Доступно:" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Có sẵn:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "可用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "可用" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "可用:" } } } }, "Badge notification overlays disabled" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Abzeichen-Benachrichtigungsüberlagerungen deaktiviert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Superposiciones de notificación de insignias desactivadas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Superpositions de notifications de badge désactivées" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lapisan pemberitahuan lencana dinonaktifkan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Sovrapposizioni di notifica badge disabilitate" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "バッジ通知オーバーレイが無効になっています" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "배지 알림 오버레이 비활성화" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nakładki powiadomień o odznakach wyłączone" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Sobreposições de notificação de emblemas desativadas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Sobreposições de notificações de emblemas desativadas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Оверлеи уведомлений значков отключены" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Prekrytia oznámení odznakov sú zakázané" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prekrivanja obvestil o značkah onemogočena" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Rozet bildirim kaplamaları devre dışı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Накладки сповіщень значків вимкнено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đã tắt lớp phủ thông báo huy hiệu" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "徽章通知覆盖已禁用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "徽章通知覆蓋已禁用" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "徽章通知覆蓋已禁用" } } } }, "Badge notification overlays enabled" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Badge-Benachrichtigungsüberlagerungen aktiviert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Superposiciones de notificación de insignias habilitadas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Superpositions de notifications de badge activées" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tampilan notifikasi lencana diaktifkan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Sovrapposizioni di notifica badge abilitate" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "バッジ通知オーバーレイが有効" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "배지 알림 오버레이 활성화" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nakładki powiadomień o odznakach włączone" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Sobreposições de notificação de emblema ativadas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Sobreposições de notificações de emblemas ativadas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Включены значки уведомлений" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Prekrytia oznámení odznakov povolené" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prekrivanja obvestil o značkah omogočena" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Rozet bildirim kaplamaları etkin" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Увімкнено накладання значків сповіщень" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đã bật lớp phủ thông báo huy hiệu" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "已启用徽章通知覆盖" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "已啟用徽章通知覆蓋" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "已啟用徽章通知覆蓋" } } } }, "Bill of Materials" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Stückliste" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Lista de Materiales" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Nomenclature" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Daftar Bahan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Distinta materiali" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "部品表" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "패키지 내용 명세서" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Lista materiałów" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Lista de Materiais" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Lista de Materiais" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Ведомость материалов" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Materiálový list" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Seznam materialov" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Malzeme Listesi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Специфікація матеріалів" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hóa đơn nguyên vật liệu" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "物料清单" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "材料清單" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "物料清單" } } } }, "Bottled (pre-built binary)" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Abgefüllt (vorgefertigte Binärdatei)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Embotellado (binario preconstruido)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Emballé (binaire pré-construit)" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Dikemas (biner yang sudah dibangun)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Imbottigliato (binario pre-costruito)" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ボトル入り(事前構築済みバイナリ)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "병에 담김 (사전 빌드된 바이너리)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Butelkowany (wstępnie zbudowany binarny)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Engarrafado (binário pré-construído)" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Engarrafado (binário pré-construído)" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Упаковано (предварительно собранный бинарный файл)" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Fľaškovaný (predkompilovaný binárny)" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ustekleničeno (vnaprej izdelan binarni)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Şişelenmiş (önceden oluşturulmuş ikili)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Розлито (попередньо зібраний бінарний файл)" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đóng chai (tệp nhị phân dựng sẵn)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "瓶装(预建二进制)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "瓶裝(預建二進制)" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "瓶裝(預建二進制)" } } } }, "BTM reset complete. Please reinstall helper." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "BTM-Reset abgeschlossen. Bitte installieren Sie den Helfer neu." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Restablecimiento de BTM completo. Por favor, reinstale el asistente." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Réinitialisation de BTM terminée. Veuillez réinstaller l'assistant." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Reset BTM selesai. Silakan instal ulang pembantu." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Reimpostazione BTM completata. Si prega di reinstallare l'assistente." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "BTMのリセットが完了しました。ヘルパーを再インストールしてください。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "BTM 재설정 완료. 도우미를 다시 설치하세요." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Reset BTM zakończony. Proszę ponownie zainstalować pomocnika." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Redefinição do BTM concluída. Por favor, reinstale o assistente." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Reinicialização do BTM concluída. Por favor, reinstale o assistente." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Сброс BTM завершен. Пожалуйста, переустановите помощник." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Reset BTM dokončený. Prosím, preinštalujte pomocníka." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ponastavitev BTM je končana. Prosimo, ponovno namestite pomočnika." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "BTM sıfırlama tamamlandı. Lütfen yardımcıyı yeniden yükleyin." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Скидання BTM завершено. Будь ласка, перевстановіть помічник." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đặt lại BTM hoàn tất. Vui lòng cài đặt lại trình trợ giúp." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "BTM 重置完成。请重新安装助手。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "BTM重置完成。請重新安裝助手。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "BTM 重置完成。請重新安裝助手。" } } } }, "BTM reset failed: %@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "BTM-Reset fehlgeschlagen: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Error al restablecer BTM: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Échec de la réinitialisation de BTM : %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Reset BTM gagal: %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Reimpostazione BTM non riuscita: %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "BTM のリセットに失敗しました: %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "BTM 재설정 실패: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Reset BTM nie powiódł się: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Falha na redefinição do BTM: %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Falha na reposição do BTM: %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Сброс BTM не удался: %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Reset BTM zlyhal: %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ponastavitev BTM ni uspela: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "BTM sıfırlama başarısız: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Скидання BTM не вдалося: %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đặt lại BTM thất bại: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "BTM 重置失败:%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "BTM 重置失敗:%@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "BTM 重置失敗:%@" } } } }, "Build only:" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Nur bauen:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Solo construir:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Construire uniquement :" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hanya membangun:" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Costruisci solo:" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ビルドのみ:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "빌드 전용:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Tylko buduj:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Apenas construir:" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Apenas construir:" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Только сборка:" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Iba zostaviť:" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Samo gradnja:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yalnızca derle:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Тільки збірка:" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chỉ xây dựng:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "仅构建:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "僅建置:" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "僅建置:" } } } }, "Bulk Delete" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Massenlöschung" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminación masiva" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Suppression en masse" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus Massal" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Eliminazione di massa" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "一括削除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "대량 삭제" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuwanie zbiorcze" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Exclusão em Massa" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar em Massa" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Массовое удаление" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Hromadné odstránenie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Masovno brisanje" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Toplu Silme" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Масове видалення" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa hàng loạt" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "批量删除" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "批量刪除" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "批量刪除" } } } }, "Bundle Files..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Bundle Dateien…" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Agrupar archivos..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Archiver les fichiers…" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Bundel Berkas…" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Raggruppa file..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "バンドルファイル..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "파일 묶기..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pakiet plików..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Arquivos do Pacote..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Agrupar Ficheiros..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Файлы пакета..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Balík súborov…" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Združi datoteke..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Paket Dosyalar…" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Пакетні Файли..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Nhóm tệp" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "捆绑文件..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "套裝檔案⋯" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "套裝檔案⋯" } } } }, "Bundle ID:" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Paket-ID:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "ID del paquete:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "ID de bundle:" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "ID Bundel:" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "ID Bundle:" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "バンドルID:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "번들 ID:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Identyfikator pakietu:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "ID do pacote:" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "ID do Pacote:" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "ID пакета:" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Identifikátor balíka:" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "ID paketa:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Paket Kimliği:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ідентифікатор пакета:" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "ID gói:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "捆绑包ID:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "套件 ID:" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "套件 ID:" } } } }, "Bundle thinning (lipo) is an aggressive operation that modifies the binaries within app bundles by removing unused architectures. While generally safe, some applications may experience issues or fail to launch after this process. It is strongly recommended to create a backup of your applications before proceeding, especially for critical or frequently used apps." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Das Bündel-Dünnen (lipo) ist ein aggressiver Vorgang, der die Binärdateien innerhalb von App-Bündeln modifiziert, indem ungenutzte Architekturen entfernt werden. Obwohl es im Allgemeinen sicher ist, können einige Anwendungen nach diesem Prozess Probleme haben oder nicht starten. Es wird dringend empfohlen, vor dem Fortfahren ein Backup Ihrer Anwendungen zu erstellen, insbesondere für kritische oder häufig verwendete Apps." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "La reducción de paquetes (lipo) es una operación agresiva que modifica los binarios dentro de los paquetes de aplicaciones eliminando arquitecturas no utilizadas. Aunque generalmente es segura, algunas aplicaciones pueden experimentar problemas o no iniciarse después de este proceso. Se recomienda encarecidamente crear una copia de seguridad de sus aplicaciones antes de proceder, especialmente para aplicaciones críticas o de uso frecuente." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "L'amincissement des bundles (lipo) est une opération agressive qui modifie les binaires au sein des bundles d'applications en supprimant les architectures inutilisées. Bien que généralement sûr, certaines applications peuvent rencontrer des problèmes ou ne pas se lancer après ce processus. Il est fortement recommandé de créer une sauvegarde de vos applications avant de procéder, en particulier pour les applications critiques ou fréquemment utilisées." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Penipisan bundel (lipo) adalah operasi agresif yang memodifikasi biner dalam bundel aplikasi dengan menghapus arsitektur yang tidak digunakan. Meskipun umumnya aman, beberapa aplikasi mungkin mengalami masalah atau gagal diluncurkan setelah proses ini. Sangat disarankan untuk membuat cadangan aplikasi Anda sebelum melanjutkan, terutama untuk aplikasi yang penting atau sering digunakan." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Il diradamento del bundle (lipo) è un'operazione aggressiva che modifica i binari all'interno dei pacchetti delle app rimuovendo le architetture non utilizzate. Sebbene generalmente sicuro, alcune applicazioni potrebbero riscontrare problemi o non avviarsi dopo questo processo. Si consiglia vivamente di creare un backup delle applicazioni prima di procedere, soprattutto per le app critiche o utilizzate frequentemente." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "バンドルシニング(lipo)は、未使用のアーキテクチャを削除することでアプリバンドル内のバイナリを変更する積極的な操作です。通常は安全ですが、一部のアプリケーションはこのプロセス後に問題が発生したり起動に失敗する可能性があります。この操作を進める前に、特に重要または頻繁に使用するアプリのバックアップを作成することを強くお勧めします。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "번들 축소(Lipo)는 사용하지 않는 아키텍처를 제거하여 앱 번들 내의 바이너리를 수정하는 매우 공격적인 작업입니다. 보통은 안전하지만, 일부 애플리케이션은 과정 후에 문제가 발생하거나 실행되지 않을 수 있습니다. 특히 중요한 앱이나 자주 사용하는 앱일 경우엔 진행하기 전에 애플리케이션 백업을 만들어 두는 것이 강력히 권장됩니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zmniejszanie rozmiaru pakietu (lipo) to agresywna operacja, która modyfikuje pliki binarne w pakietach aplikacji poprzez usunięcie nieużywanych architektur. Chociaż jest to ogólnie bezpieczne, niektóre aplikacje mogą mieć problemy lub nie uruchamiać się po tym procesie. Zdecydowanie zaleca się utworzenie kopii zapasowej aplikacji przed kontynuowaniem, zwłaszcza w przypadku aplikacji krytycznych lub często używanych." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "O afinamento de pacotes (lipo) é uma operação agressiva que modifica os binários dentro dos pacotes de aplicativos removendo arquiteturas não utilizadas. Embora geralmente seguro, alguns aplicativos podem apresentar problemas ou falhar ao iniciar após esse processo. É altamente recomendável criar um backup de seus aplicativos antes de prosseguir, especialmente para aplicativos críticos ou frequentemente usados." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "O afinamento de pacotes (lipo) é uma operação agressiva que modifica os binários dentro dos pacotes de aplicativos, removendo arquiteturas não utilizadas. Embora geralmente seguro, algumas aplicações podem apresentar problemas ou falhar ao iniciar após este processo. É fortemente recomendado criar uma cópia de segurança das suas aplicações antes de prosseguir, especialmente para aplicações críticas ou frequentemente utilizadas." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Уменьшение пакета (lipo) — это агрессивная операция, которая модифицирует бинарные файлы в пакетах приложений, удаляя неиспользуемые архитектуры. Хотя в целом это безопасно, некоторые приложения могут столкнуться с проблемами или не запускаться после этого процесса. Настоятельно рекомендуется создать резервную копию ваших приложений перед продолжением, особенно для критически важных или часто используемых приложений." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zúženie balíka (lipo) je agresívna operácia, ktorá modifikuje binárne súbory v rámci balíkov aplikácií odstránením nepoužívaných architektúr. Hoci je všeobecne bezpečná, niektoré aplikácie môžu po tomto procese zaznamenať problémy alebo sa nemusia spustiť. Dôrazne sa odporúča vytvoriť zálohu vašich aplikácií pred pokračovaním, najmä pre kritické alebo často používané aplikácie." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Redčenje svežnjev (lipo) je agresivna operacija, ki spreminja binarne datoteke znotraj svežnjev aplikacij z odstranjevanjem neuporabljenih arhitektur. Čeprav je običajno varno, lahko nekatere aplikacije po tem procesu naletijo na težave ali se ne zaženejo. Močno priporočamo, da pred nadaljevanjem ustvarite varnostno kopijo svojih aplikacij, še posebej za kritične ali pogosto uporabljene aplikacije." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Paket inceltme (lipo), kullanılmayan mimarileri kaldırarak uygulama paketlerindeki ikili dosyaları değiştiren agresif bir işlemdir. Genellikle güvenli olsa da, bazı uygulamalar bu işlemden sonra sorun yaşayabilir veya başlatılamayabilir. Özellikle kritik veya sık kullanılan uygulamalar için devam etmeden önce uygulamalarınızın yedeğini oluşturmanız şiddetle tavsiye edilir." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Зменшення пакета (lipo) — це агресивна операція, яка змінює двійкові файли в пакетах додатків, видаляючи невикористовувані архітектури. Хоча це зазвичай безпечно, деякі додатки можуть зіткнутися з проблемами або не запускатися після цього процесу. Настійно рекомендується створити резервну копію ваших додатків перед продовженням, особливо для критичних або часто використовуваних додатків." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Gọt mỏng gói (lipo) là một thao tác mạnh mẽ thay đổi các tệp nhị phân trong gói ứng dụng bằng cách loại bỏ các kiến trúc không sử dụng. Mặc dù thường an toàn, một số ứng dụng có thể gặp sự cố hoặc không khởi động được sau quá trình này. Rất khuyến nghị tạo bản sao lưu ứng dụng của bạn trước khi tiếp tục, đặc biệt đối với các ứng dụng quan trọng hoặc thường xuyên sử dụng." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "捆绑瘦身(lipo)是一种激进的操作,通过移除未使用的架构来修改应用程序捆绑包中的二进制文件。虽然通常是安全的,但某些应用程序在此过程后可能会出现问题或无法启动。强烈建议在继续之前为您的应用程序创建备份,尤其是对于关键或经常使用的应用程序。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "捆綁瘦身(lipo)是一種激進的操作,通過移除未使用的架構來修改應用程式包內的二進位檔案。雖然通常是安全的,但某些應用程式可能會在此過程後遇到問題或無法啟動。強烈建議在繼續之前為您的應用程式創建備份,尤其是對於關鍵或經常使用的應用程式。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "套件瘦身 (lipo) 是一種積極的操作,通過移除未使用的架構來修改應用程式套件內的二進制檔案。雖然通常是安全的,但某些應用程式可能會在此過程後出現問題或無法啟動。強烈建議在進行此操作之前備份您的應用程式,特別是對於重要或經常使用的應用程式。" } } } }, "Bundle thinning complete.\nTotal space saved from all binaries in bundle." : { "comment" : "Lipo result message", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Bündel-Dünnung abgeschlossen Gesamter Speicherplatz, der durch alle Binärdateien im Bündel eingespart wurde" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Adelgazamiento del paquete completo Total de espacio ahorrado de todos los binarios en el paquete" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Amincissement du bundle terminé. Espace total économisé par tous les binaires dans le bundle." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Penipisan bundel selesai. Total ruang yang dihemat dari semua biner dalam bundel." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Snellimento del bundle completato.\nSpazio totale risparmiato da tutti i binari nel bundle." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "バンドルのスリム化が完了しました。バンドル内のすべてのバイナリから節約された合計スペース。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "번들 축소 완료.\n번들 내 모든 바이너리에서 절약된 총 공간." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zakończono zmniejszanie rozmiaru pakietu.\nCałkowita ilość miejsca zaoszczędzona dzięki wszystkim plikom binarnym w pakiecie." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "A otimização do pacote está completa. Espaço total economizado de todos os binários no pacote." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A redução do pacote está completa.\nEspaço total economizado de todos os binários no pacote." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Сокращение пакета завершено. Общий объем сэкономленного пространства от всех бинарных файлов в пакете." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Dokončené zmenšovanie balíka. Celkový ušetrený priestor zo všetkých binárnych súborov v balíku." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Redčenje paketa je končano.\nSkupni prihranek prostora iz vseh binarnih datotek v paketu." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Paket inceltme tamamlandı. Paketteki tüm ikili dosyalardan tasarruf edilen toplam alan." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Завершено оптимізацію пакета. Загальний обсяг збереженого простору з усіх бінарних файлів у пакеті." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hoàn tất giảm kích thước gói. Tổng không gian tiết kiệm từ tất cả các tệp nhị phân trong gói." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "捆绑瘦身完成。捆绑中所有二进制文件总共节省的空间。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "捆綁精簡完成。捆綁中所有二進制文件節省的總空間。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "完成精簡組合。從組合中的所有二進制檔案中節省的總空間。" } } } }, "Bundle: %@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Paket: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Paquete: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Paquet: %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Paket: %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Pacchetto: %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "バンドル: %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "번들: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pakiet: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Pacote: %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Pacote: %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Пакет: %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Balík: %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Paket: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Paket: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Пакет: %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Gói: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "程序包:%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "包:%@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "套件:%@" } } } }, "Cache Size:" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Cache-Größe:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Tamaño de caché:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Taille du cache:" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ukuran Tembolok:" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Dimensione cache:" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "キャッシュサイズ:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "캐시 크기:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Rozmiar pamięci podręcznej:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Tamanho do cache:" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Tamanho da Cache:" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Размер кэша:" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Veľkosť vyrovnávacej pamäte:" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Velikost predpomnilnika:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Önbellek Boyutu:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Розмір кешу:" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Kích thước bộ nhớ đệm:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "缓存大小:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "快取大小:" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "快取大小:" } } } }, "calculating" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "berechnen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "calculando" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "calcul en cours" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "menghitung" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "calcolando" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "計算中" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "계산 중" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "obliczanie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "calculando" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "a calcular" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "вычисление" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "vypočítavanie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "izračunavanje" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "hesaplanıyor" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "розраховую" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "đang tính toán" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "正在计算" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "正在計算" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "正在計算" } } } }, "Calculating..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Berechnung läuft..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Calculando..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Calcul en cours..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Menghitung..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Calcolando..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "計算中..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "계산 중..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Obliczanie..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Calculando..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A calcular..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Вычисление..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Počítam..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izračunavam..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hesaplanıyor..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Обчислення..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang tính toán..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "正在计算..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "計算中..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "計算中..." } } } }, "Cancel" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Abbrechen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cancelar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Annuler" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Batalkan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Annulla" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "キャンセル" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "취소" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Anuluj" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Cancelar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Cancelar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Отмена" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zrušiť" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prekliči" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Vazgeç" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Скасувати" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hủy" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "取消" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "取消" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "取消" } } } }, "Cancel running Homebrew operation" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Laufende Homebrew-Operation abbrechen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cancelar operación de Homebrew en curso" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Annuler l'opération Homebrew en cours" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Batalkan operasi Homebrew yang sedang berjalan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Annulla operazione Homebrew in corso" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "実行中のHomebrew操作をキャンセル" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "실행 중인 Homebrew 작업 취소" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Anuluj działanie Homebrew" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Cancelar operação do Homebrew em andamento" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Cancelar operação Homebrew em execução" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Отменить выполняемую операцию Homebrew" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zrušiť prebiehajúcu operáciu Homebrew" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prekliči trenutno operacijo Homebrew" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Çalışan Homebrew işlemini iptal et" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Скасувати виконувану операцію Homebrew" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hủy bỏ hoạt động Homebrew đang chạy" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "取消正在运行的 Homebrew 操作" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "取消正在運行的 Homebrew 操作" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "取消正在運行的 Homebrew 操作" } } } }, "Cannot hide active startup page" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Aktive Startseite kann nicht ausgeblendet werden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No se puede ocultar la página de inicio activa" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Impossible de masquer la page de démarrage active" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak dapat menyembunyikan halaman startup aktif" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Impossibile nascondere la pagina di avvio attiva" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アクティブなスタートアップページを非表示にできません" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "활성 시작 페이지를 숨길 수 없습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nie można ukryć aktywnej strony startowej" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Não é possível ocultar a página inicial ativa" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Não é possível ocultar a página inicial ativa" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Невозможно скрыть активную стартовую страницу" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nie je možné skryť aktívnu úvodnú stránku" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ni mogoče skriti aktivne začetne strani" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Etkin başlangıç sayfası gizlenemiyor" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Неможливо приховати активну стартову сторінку" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không thể ẩn trang khởi động đang hoạt động" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "无法隐藏活动的启动页面" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "無法隱藏活動啟動頁面" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "無法隱藏活動啟動頁面" } } } }, "Case sensitive" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Groß-/Kleinschreibung beachten" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Distinguir mayúsculas y minúsculas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Sensible à la casse" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Peka huruf besar-kecil" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Distinzione tra maiuscole e minuscole" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "大文字と小文字を区別" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "대소문자 구분" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Rozróżnianie wielkości liter" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Diferencia maiúsculas de minúsculas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Diferencia maiúsculas de minúsculas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "С учетом регистра" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Rozlišovať veľké a malé písmená" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Razlikovanje velikih in malih črk" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Büyük/küçük harf duyarlı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "З урахуванням регістру" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Phân biệt chữ hoa chữ thường" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "区分大小写" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "區分大小寫" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "區分大小寫" } } } }, "Cask not found" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Fass nicht gefunden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Barril no encontrado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Fût introuvable" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tong tidak ditemukan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Cask non trovato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "樽が見つかりません" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "캐스크를 찾을 수 없습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Beczka nie znaleziona" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Barril não encontrado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Barril não encontrado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Бочка не найдена" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Sud nenájdený" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Sod ni bil najden" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Fıçı bulunamadı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Бочку не знайдено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không tìm thấy thùng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "找不到木桶" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "找不到桶" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "找不到桶" } } } }, "Casks" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Fässer" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Barriles" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Fûts" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tong" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Casks" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "樽" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "캐스크" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Beczki" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Barricas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Barricas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Бочки" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Sud" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Casks" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Fıçı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Бочки" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thùng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "桶" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "桶" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "桶" } } } }, "Casks (%lld)" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Fässer (%lld)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Barriles (%lld)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Fûts (%lld)" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tong (%lld)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Casks (%lld)" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "樽 (%lld)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "캐스크 (%lld)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Beczki (%lld)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Barricas (%lld)" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Barriletes (%lld)" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Бочки (%lld)" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Sud (%lld)" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Casks (%lld)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Fıçı (%lld)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Бочки (%lld)" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thùng (%lld)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "桶 (%lld)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "桶 (%lld)" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "桶 (%lld)" } } } }, "Categories" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Kategorien" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Categorías" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Catégories" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Kategori" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Categorie" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "カテゴリー" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "카테고리" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Kategorie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Categorias" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Categorias" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Категории" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Kategórie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Kategorije" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kategoriler" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Категорії" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Danh mục" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "类别" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "類別" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "類別" } } } }, "Category: %@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Kategorie: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Categoría: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Catégorie : %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Kategori: %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Categoria: %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "カテゴリ: %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "카테고리: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Kategoria: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Categoria: %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Categoria: %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Категория: %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Kategória: %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Kategorija: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kategori: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Категорія: %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Danh mục: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "类别:%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "類別:%@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "類別:%@" } } } }, "Change" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ändern" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cambiar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Modifier" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ubah" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Modifica" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "変更" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "변경" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zmień" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Alterar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Alterar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Изменить" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zmeniť" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Spremeni" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Değiştir" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Змінити" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thay đổi" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "更改" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "更改" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "更改" } } } }, "Check for potential issues" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Auf potenzielle Probleme prüfen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Verificar problemas potenciales" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Vérifier les problèmes potentiels" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Periksa potensi masalah" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Verifica potenziali problemi" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "潜在的な問題を確認する" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "잠재적 문제 확인" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Sprawdź potencjalne problemy" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Verificar problemas potenciais" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Verificar possíveis problemas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Проверить на наличие потенциальных проблем" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Skontrolujte potenciálne problémy" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Preveri morebitne težave" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Olası sorunları kontrol et" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Перевірте на наявність потенційних проблем" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Kiểm tra các vấn đề tiềm ẩn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "检查潜在问题" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "檢查潛在問題" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "檢查潛在問題" } } } }, "Check for Updates" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Auf Updates überprüfen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Buscar actualizaciones" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Vérifier si une mise à jour est disponible…" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Periksa Pembaruan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Controlla aggiornamenti" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "更新を確認" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "업데이트 확인" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Sprawdź aktualizacje" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Verificar atualizações" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Verificar atualizações" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Проверить на обновления" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Skontrolovať aktualizácie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Preveri za posodobitve" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Güncellemeleri Kontrol Et" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Перевірити Оновлення" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Kiểm tra cập nhật" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "检查更新" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "檢查更新" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "檢查更新" } } } }, "Check out the latest additions to Pearcleaner." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Schauen Sie sich die neuesten Ergänzungen zu Pearcleaner an." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Descubre las últimas incorporaciones a Pearcleaner." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Découvrez les dernières nouveautés de Pearcleaner." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lihat tambahan terbaru di Pearcleaner." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Scopri le ultime aggiunte a Pearcleaner." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner の最新の追加機能をチェックしてください。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner의 최신 추가 기능을 확인하세요." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Sprawdź najnowsze dodatki do Pearcleaner." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Confira as últimas adições ao Pearcleaner." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Confira as últimas adições ao Pearcleaner." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Ознакомьтесь с последними дополнениями к Pearcleaner." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pozrite si najnovšie prírastky do Pearcleaner." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Oglejte si najnovejše dodatke k Pearcleanerju." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner'a en son eklenenleri inceleyin." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ознайомтеся з останніми доповненнями до Pearcleaner." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xem các bổ sung mới nhất cho Pearcleaner." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "查看 Pearcleaner 的最新添加内容。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "查看 Pearcleaner 的最新新增功能。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "查看 Pearcleaner 的最新新增功能。" } } } }, "Checking..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Prüfen..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Comprobando..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Vérification..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Memeriksa..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Verifica in corso..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "確認中..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "확인 중..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Sprawdzanie…" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Verificando..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A verificar..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Проверка..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Kontrolujem..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Preverjanje..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kontrol ediliyor..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Перевірка..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang kiểm tra…" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "正在检查…" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "正在檢查⋯" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "正在檢查⋯" } } } }, "Choose Folder..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ordner auswählen..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Elegir carpeta..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Choisir un dossier..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pilih Folder..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Scegli cartella..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "フォルダーを選択..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "폴더 선택..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wybierz folder..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Escolher Pasta..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Escolher Pasta..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Выбрать папку..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vybrať priečinok..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izberite mapo..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Klasör Seç..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Вибрати папку..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chọn thư mục..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "选择文件夹..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "選擇資料夾..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "選擇資料夾..." } } } }, "Choose Languages..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Sprachen auswählen..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Elegir idiomas..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Choisir les langues..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pilih Bahasa..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Scegli lingue..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "言語を選択..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "언어 선택..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wybierz języki..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Escolher idiomas..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Escolher Idiomas..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Выбрать языки..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vyberte jazyky..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izberi jezike..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Dilleri Seç..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Вибрати мови..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chọn ngôn ngữ..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "选择语言..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "選擇語言..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "選擇語言..." } } } }, "Clean stored files and cache for common IDEs" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Gespeicherte Dateien und Cache für gängige IDEs bereinigen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Limpiar archivos almacenados y caché de IDEs comunes" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Nettoyer les fichiers stockés et le cache pour les IDE courants" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Bersihkan file yang disimpan dan tembolok untuk IDE umum" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Pulisci i file memorizzati e la cache per gli IDE comuni" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "一般的なIDEの保存ファイルとキャッシュをクリーンにする" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "대다수 IDE의 저장된 파일 및 캐시 정리" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyczyść przechowywane pliki i pamięć podręczną dla popularnych środowisk IDE" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Limpar arquivos armazenados e cache para IDEs comuns" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Limpar arquivos armazenados e cache para IDEs comuns" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Очистить сохраненные файлы и кеш для популярных IDE" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vyčistiť uložené súbory a cache pre bežné IDEs" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Počisti shranjene datoteke in predpomnilnik za običajne IDE-je" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yaygın IDE'ler için depolanan dosyaları ve önbelleği temizle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Очистити збережені файли та кеш для поширених середовищ розробки" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Dọn dẹp tệp được lưu trữ và bộ nhớ cache cho các IDE phổ biến" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "清理常用 IDE 的存储文件和缓存" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "清理常用 IDE 的存儲文件和緩存" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "清理常見IDE的存檔和快取" } } } }, "Cleaning..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Reinigung..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Limpiando..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Nettoyage..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Membersihkan..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Pulizia in corso..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "クリーニング中..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "정리 중..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Czyszczenie..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Limpando..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A limpar..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Очистка..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Čistenie..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Čiščenje..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Temizleniyor..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Очищення..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang dọn dẹp..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "清理中..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "清理中..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "清理中..." } } } }, "Cleanup" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Bereinigung" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Limpieza" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Nettoyage" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pembersihan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Pulizia" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "クリーンアップ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "정리" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Czyszczenie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Limpeza" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Limpeza" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Очистка" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vyčistenie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Čiščenje" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Temizlik" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Очистка" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Dọn dẹp" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "清理" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "清理" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "清理" } } } }, "Clear" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Löschen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Borrar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Effacer" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Bersihkan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Cancella" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "クリア" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "지우기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyraźny" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Limpar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Limpar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Очистить" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vyčistiť" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Počisti" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Temizle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Очистити" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "透视" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "透视" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "透视" } } } }, "Clear cached credentials immediately" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Zwischengespeicherte Anmeldeinformationen sofort löschen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Borrar credenciales en caché inmediatamente" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Effacer immédiatement les informations d'identification mises en cache" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Segera hapus kredensial yang di-cache" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Cancella immediatamente le credenziali memorizzate nella cache" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "キャッシュされた資格情報を即座にクリア" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "캐시된 자격 증명을 즉시 지우기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyczyść pamięć podręczną poświadczeń natychmiast" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Limpar credenciais em cache imediatamente" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Limpar credenciais em cache imediatamente" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Очистить кэшированные учетные данные немедленно" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Okamžite vymazať uložené poverenia" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Takoj izbrišite predpomnjene poverilnice" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Önbelleğe alınmış kimlik bilgilerini hemen temizle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Негайно очистити кешовані облікові дані" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa thông tin xác thực được lưu trữ ngay lập tức" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "立即清除缓存的凭证" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "立即清除緩存的憑證" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "立即清除緩存的憑證" } } } }, "Clear console output" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Konsolenausgabe löschen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Limpiar salida de consola" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Effacer la sortie de la console" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Bersihkan keluaran konsol" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Cancella l'output della console" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "コンソール出力をクリア" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "콘솔 출력 지우기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyczyść wyjście konsoli" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Limpar saída do console" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Limpar saída do console" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Очистить вывод консоли" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vymazať výstup konzoly" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Počisti izhod konzole" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Konsol çıktısını temizle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Очистити вивід консолі" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa đầu ra bảng điều khiển" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "清除控制台输出" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "清除控制台輸出" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "清除控制台輸出" } } } }, "Clear text" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Text löschen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Borrar texto" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Effacer le texte" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus teks" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Cancella testo" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "テキストをクリア" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "텍스트 지우기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyczyść tekst" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Limpar texto" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Limpar texto" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Очистить текст" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vyčistiť text" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Počisti besedilo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Metni temizle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Очистити текст" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa văn bản" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "清除文本" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "清除文字" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "清除文字" } } } }, "Click for apps list" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Klicke für Programmliste" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Haz clic para ver la lista de aplicaciones" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Cliquer pour voir la liste des applications" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Klik untuk melihat daftar aplikasi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Clicca per l'elenco delle app" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アプリリストを表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "앱 목록을 보려면 클릭하세요" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Kliknij, aby wyświetlić listę aplikacji" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Clique para ver a lista de aplicativos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Clique para lista de aplicativos" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Нажмите, чтобы перейти к списку приложений" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Klikni pre zoznam aplikácií" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Kliknite za seznam aplikacij" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uygulama listesi için tıklayın" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Натисни для списку програм" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Nhấp để xem danh sách ứng dụng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "点击查看应用列表" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "按一下以取得 App 列表" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "按一下以取得 App 列表" } } } }, "Click header to change sorting order" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Klicke auf die Kopfzeile, um die Sortierreihenfolge zu ändern" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Haz clic en el encabezado para cambiar el orden de clasificación" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Cliquer sur l'en-tête pour modifier l'ordre de tri" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Klik header untuk mengubah urutan penyortiran" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Clicca sull'intestazione per cambiare l'ordine di ordinamento" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ヘッダーをクリックして並べ替え順序を変更" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "정렬 순서를 변경하려면 헤더를 클릭하세요" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Kliknij nagłówek, aby zmienić kolejność sortowania" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Clique no cabeçalho para alterar a ordem de classificação" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Clique no cabeçalho para alterar a ordem de classificação" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Нажмите на заголовок, чтобы изменить порядок сортировки" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Kliknutím na hlavičku zmeň poradie zoradenia" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Kliknite glavo za spremembo vrstnega reda razvrščanja" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sıralama düzenini değiştirmek için başlığa tıklayın" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Натисни на заголовок для зміни сортування" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Nhấn vào tiêu đề để thay đổi thứ tự sắp xếp" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "点击标题更改排序顺序" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "按一下標題以更改排列順序" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "按一下標題以更改排列順序" } } } }, "Click to dismiss" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Zum Schließen klicken" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Haz clic para descartar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Cliquez pour ignorer" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Klik untuk menutup" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Clicca per chiudere" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "クリックして閉じる" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "클릭하여 닫기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Kliknij, aby zamknąć" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Clique para dispensar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Clique para dispensar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Нажмите, чтобы закрыть" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Kliknite pre zatvorenie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Kliknite za zapiranje" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kapatmak için tıklayın" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Натисніть, щоб закрити" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Nhấp để đóng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "点击关闭" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "點擊以關閉" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "按此關閉" } } } }, "Click to search" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Klicke für die Suche" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Haga clic para buscar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Cliquer pour rechercher" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Klik untuk mencari" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Clicca per cercare" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "検索するにはクリック" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "검색하려면 클릭" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Kliknij, aby wyszukać" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Clique para buscar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Clique para pesquisar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Нажмите, чтобы найти" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Klikni pre vyhľadávanie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Kliknite za iskanje" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Aramak için tıklayın" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Натисни для пошуку" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Nhấn để tìm kiếm" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "点击以搜索" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "按一下以搜尋" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "按一下以搜尋" } } } }, "Click to view issues" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Klicken, um Probleme anzuzeigen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Haga clic para ver problemas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Cliquez pour voir les problèmes" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Klik untuk melihat masalah" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Clicca per visualizzare i problemi" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "問題を見るにはクリックしてください" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "문제를 보려면 클릭하세요" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Kliknij, aby zobaczyć problemy" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Clique para ver os problemas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Clique para ver problemas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Нажмите, чтобы просмотреть проблемы" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Kliknite na zobrazenie problémov" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Kliknite za ogled težav" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sorunları görüntülemek için tıklayın" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Натисніть, щоб переглянути проблеми" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Nhấp để xem các vấn đề" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "点击查看问题" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "點擊查看問題" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "點擊查看問題" } } } }, "Close" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Schliessen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cerrar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Fermer" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tutup" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Chiudi" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "閉じる" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "닫기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zamknij" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Fechar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Fechar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Закрыть" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zatvoriť" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Zapri" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kapat" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Закрити" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đóng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "关闭" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "關閉" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "關閉" } } } }, "Close after uninstall" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Schliesse nach dem Entfernen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cerrar después de desinstalar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Fermer après désinstallation" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tutup setelah mencopot pemasangan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Chiudi dopo la disinstallazione" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アンインストール後に閉じる" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "제거 후 닫기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zamknij po odinstalowaniu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Fechar após desinstalar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Fechar após desinstalar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Закрыть после удаления" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zatvoriť po odinštalácii" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Zapri po odstranitvi" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kaldırdıktan sonra kapat" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Закрити після видалення" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đóng sau khi gỡ ứng dụng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "卸载后关闭" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "解除安裝後關閉" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "解除安裝後關閉" } } } }, "Close related files search" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Suche nach verwandten Dateien schließen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cerrar búsqueda de archivos relacionados" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Fermer la recherche de fichiers associés" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tutup pencarian file terkait" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Chiudi ricerca file correlati" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "関連ファイル検索を閉じる" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "관련 파일 검색 닫기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zamknij wyszukiwanie powiązanych plików" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Fechar pesquisa de arquivos relacionados" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Fechar pesquisa de arquivos relacionados" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Закрыть поиск связанных файлов" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zatvoriť vyhľadávanie súvisiacich súborov" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Zapri iskanje povezanih datotek" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İlgili dosya aramasını kapat" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Закрити пошук пов'язаних файлів" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đóng tìm kiếm tệp liên quan" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "关闭相关文件搜索" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "關閉相關檔案搜尋" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "關閉相關檔案搜尋" } } } }, "Color String" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Farbzeichenfolge" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cadena de color" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Chaîne de couleurs" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "String Warna" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Stringa di colore" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "カラーストリング" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "색상 문자열" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ciąg kolorów" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "String de Cor" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Cadeia de Cores" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Цветовая строка" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Farebná reťazec" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Barvna niz" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Renk Dizisi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Колірний рядок" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chuỗi màu sắc" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "颜色字符串" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "顏色字串" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "顏色字串" } } } }, "Command Line" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Befehlszeile" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Línea de comandos" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Interface de Ligne de Commande (CLI)" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Baris Perintah" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Riga di comando" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "コマンドライン" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "명령줄" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wiersz poleceń" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Linha de Comando" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Linha de Comando" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Командная строка" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Príkazový riadok" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ukazna vrstica" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Komut Satırı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Команда строка" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Sử dụng dòng lệnh (CLI)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "命令行" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "命令列" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "命令列" } } } }, "Comment" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Kommentar" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Comentario" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Commentaire" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Komentar" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Commento" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "コメント" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "댓글" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Komentarz" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Comentário" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Comentário" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Комментарий" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Komentár" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Komentiraj" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yorum" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Коментар" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Bình luận" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "评论" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "評論" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "評論" } } } }, "Completed 🚀" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Erledigt 🚀" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Completado 🚀" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Terminé 🚀" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Selesai 🚀" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Completato 🚀" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "完了 🚀" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "완료 🚀" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ukończono 🚀" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Concluído 🚀" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Concluído 🚀" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Завершено 🚀" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Dokončené 🚀" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Dokončano 🚀" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Tamamlandı 🚀" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Виконано 🚀" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hoàn thành 🚀" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "已完成 🚀" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "已完成 🚀" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "已完成 🚀" } } } }, "Completely remove package: delete all files, receipts, and forget package" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Paket vollständig entfernen: Alle Dateien, Belege löschen und Paket vergessen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar completamente el paquete: borrar todos los archivos, recibos y olvidar el paquete" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer complètement le paquet : supprimer tous les fichiers, reçus et oublier le paquet" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus paket sepenuhnya: hapus semua file, tanda terima, dan lupakan paket" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Rimuovi completamente il pacchetto: elimina tutti i file, le ricevute e dimentica il pacchetto" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "パッケージを完全に削除: すべてのファイル、レシートを削除し、パッケージを忘れる" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "패키지 완전 제거: 모든 파일, 영수증 삭제 및 패키지 잊기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Całkowicie usuń pakiet: usuń wszystkie pliki, paragony i zapomnij o pakiecie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Remover completamente o pacote: excluir todos os arquivos, recibos e esquecer o pacote" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Remover completamente o pacote: eliminar todos os ficheiros, recibos, e esquecer o pacote" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Полностью удалить пакет: удалить все файлы, квитанции и забыть пакет" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Úplne odstrániť balík: vymazať všetky súbory, potvrdenky a zabudnúť na balík" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Popolnoma odstrani paket: izbriši vse datoteke, potrdila in pozabi paket" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Paketi tamamen kaldır: tüm dosyaları, makbuzları sil ve paketi unut" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Повністю видалити пакет: видалити всі файли, квитанції та забути пакет" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hoàn toàn xóa gói: xóa tất cả các tệp, biên lai và quên gói" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "完全删除软件包:删除所有文件、收据,并忘记软件包" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "完全移除套件:刪除所有文件、收據,並忘記套件" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "完全移除套件:刪除所有文件、收據,並忘記套件" } } } }, "Confirmation alerts" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Bestätigungswarnungen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Alertas de confirmación" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Alertes de confirmation" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Peringatan konfirmasi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Avvisi di conferma" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "確認アラート" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "확인 알림" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Alerty potwierdzenia" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Alertas de confirmação" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Alertas de confirmação" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Уведомления о подтверждении" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Potvrdzovacie upozornenia" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Potrditvena opozorila" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Onay uyarıları" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Сповіщення про підтвердження" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Cảnh báo xác nhận" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "确认提示" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "確認提示" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "確認提示" } } } }, "Console" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Konsole" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Consola" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Console" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Konsol" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Console" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "コンソール" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "콘솔" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Konsola" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Console" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Consola" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Консоль" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Konzola" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Konzola" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Konsol" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Консоль" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Bảng điều khiển" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "控制台" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "控制台" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "控制台" } } } }, "Container" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Behälter" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Contenedor" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Conteneur" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Kontainer" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Contenitore" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "コンテナ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "컨테이너" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Kontener" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Contêiner" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Contêiner" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Контейнер" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Kontajner" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Kontejner" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Konteyner" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Контейнер" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thùng chứa" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "容器" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "容器" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "容器" } } } }, "Copy" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Kopieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Copiar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Copier" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Salin" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Copia" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "コピー" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "복사" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Kopiuj" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Copiar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Copiar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Копировать" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Kopírovať" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Kopiraj" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kopyala" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Копіювати" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Sao chép" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "复制" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "複製" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "複製" } } } }, "Copy console output to clipboard" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Konsolenausgabe in die Zwischenablage kopieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Copiar salida de consola al portapapeles" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Copier la sortie de la console dans le presse-papiers" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Salin keluaran konsol ke clipboard" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Copia l'output della console negli appunti" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "コンソール出力をクリップボードにコピー" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "콘솔 출력을 클립보드에 복사" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Skopiuj wynik konsoli do schowka" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Copiar saída do console para a área de transferência" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Copiar saída do console para a área de transferência" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Скопировать вывод консоли в буфер обмена" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Kopírovať výstup konzoly do schránky" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Kopiraj izhod konzole v odložišče" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Konsol çıktısını panoya kopyala" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Скопіювати вивід консолі в буфер обміну" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Sao chép đầu ra bảng điều khiển vào khay nhớ tạm" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "复制控制台输出到剪贴板" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "複製控制台輸出到剪貼板" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "複製控制台輸出到剪貼板" } } } }, "Copy File Paths" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Kopiere Dateipfad" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Copiar rutas de archivos" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Copier les chemins d’accès du fichier" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Salin Jalur File" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Copia percorsi file" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ファイルパスをコピー" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "파일 경로 복사" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Kopiuj ścieżki plików" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Copiar Caminhos de Arquivos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Copiar Caminhos de Ficheiros" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Скопировать путь файлов" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Kopírovať umiestnenia súboru" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Kopiraj poti datotek" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Dosya Dizinlerini Kopyala" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Копіювати шляхів до файлів" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Sao chép đường dẫn tệp" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "复制文件路径" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "複製檔案路徑" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "複製檔案路徑" } } } }, "Copy Path" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Kopiere Pfad" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Copiar ruta" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Copier le chemin d’accès" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Salin Jalur" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Copia percorso" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "パスをコピー" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "경로 복사" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Kopiuj ścieżkę" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Copiar Caminho" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Copiar Caminho" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Скопировать путь" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Kopírovať umiestnenie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Kopiraj pot" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Dizini Kopyala" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Скопіювати шлях" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Sao chép đường dẫn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "复制路径" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "複製路徑" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "複製路徑" } } } }, "Current" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Aktuell" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actual" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Actuel" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Saat ini" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Attuale" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "現在" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "현재" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Bieżący" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atual" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Atual" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Текущий" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Aktuálny" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Trenutni" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Mevcut" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Поточний" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hiện tại" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "当前" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "當前" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "當前" } } } }, "Custom Sensitivity" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Benutzerdefinierte Empfindlichkeit" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Sensibilidad personalizada" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Sensibilité personnalisée" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Sensitivitas Kustom" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Sensibilità personalizzata" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "カスタム感度" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "사용자 정의 민감도" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Indywidualna czułość" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Sensibilidade Personalizada" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Sensibilidade Personalizada" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Пользовательская чувствительность" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vlastná citlivosť" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prilagojena občutljivost" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Özel Hassasiyet" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Налаштовувана чутливість" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Độ nhạy tùy chỉnh" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "自定义灵敏度" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "自訂靈敏度" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "自訂靈敏度" } } } }, "Daily" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Täglich" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Diario" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Journalier" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Harian" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Giornaliero" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "毎日" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "일일" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dziennie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Diário" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Diário" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Ежедневно" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Denne" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Dnevno" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Günlük" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Щоденно" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hàng ngày" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "每天" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "每日" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "每日" } } } }, "Date" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Datum" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Fecha" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Date" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tanggal" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Data" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "日付" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "날짜" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Data" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Data" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Data" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Дата" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Dátum" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Datum" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Tarih" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Дата" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ngày" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "日期" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "日期" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "日期" } } } }, "Date Added" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Hinzugefügt am" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Fecha añadida" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Date ajoutée" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tanggal Ditambahkan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Data aggiunta" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "追加日" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "추가된 날짜" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Data dodania" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Data Adicionada" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Data de Adição" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Дата добавления" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Dátum pridania" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Datum dodano" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Eklenme Tarihi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Дата додавання" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ngày thêm" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "添加日期" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "新增日期" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "新增日期" } } } }, "Date Created" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Erstellungsdatum" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Fecha de creación" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Date de création" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tanggal Dibuat" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Data di creazione" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "作成日" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "생성 날짜" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Data utworzenia" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Data de Criação" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Data de Criação" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Дата создания" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Dátum vytvorenia" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Datum ustvarjanja" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Oluşturulma Tarihi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Дата створення" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ngày tạo" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "创建日期" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "建立日期" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "建立日期" } } } }, "Debug" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Debuggen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Depuración" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Débogage" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Debug" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Debug" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "デバッグ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "디버그" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Debugowanie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Depuração" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Depuração" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Отладка" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Ladenie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odpravljanje napak" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hata ayıklama" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Налагодження" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Gỡ lỗi" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "调试" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "除錯" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "調試" } } } }, "Debug Console" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Debug-Konsole" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Consola de Depuración" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Console de débogage" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Konsol Debug" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Console di debug" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "デバッグコンソール" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "디버그 콘솔" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Konsola debugowania" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Console de Depuração" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Consola de Depuração" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Консоль отладки" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Ladiaca konzola" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Konzola za odpravljanje napak" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hata Ayıklama Konsolu" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Консоль Налагодження" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Bảng điều khiển gỡ lỗi" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "调试控制台" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "除錯主控台" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "除錯主控台" } } } }, "Deep" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Tief" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Profundo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Profond" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Dalam" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Profondo" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "深い" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "심층" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Głęboki" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Profundo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Profundo" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Глубокий" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Hlboký" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Globoko" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Derin" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Глибокий" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Sâu" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "深" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "深" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "深" } } } }, "Deep Search Level" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Tiefensuchstufe" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Nivel de búsqueda profunda" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Niveau de recherche approfondie" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tingkat Pencarian Mendalam" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Livello di ricerca profonda" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ディープサーチレベル" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "심층 검색 수준" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Poziom głębokiego wyszukiwania" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nível de Pesquisa Profunda" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Nível de Pesquisa Profunda" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Уровень глубокого поиска" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Hlboká úroveň vyhľadávania" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Globoka raven iskanja" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Derin Arama Seviyesi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Рівень глибокого пошуку" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Cấp độ tìm kiếm sâu" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "深度搜索级别" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "深度搜尋級別" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "深度搜尋級別" } } } }, "Delete" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Löschen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Elimina" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "削除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "삭제" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Excluir" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vymazať" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izbriši" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sil" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "删除" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "刪除" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "刪除" } } } }, "Delete %@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Löschen %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Elimina %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%@ を削除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@ 삭제" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Excluir %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstrániť %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izbriši %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sil %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "删除 %@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "刪除 %@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "刪除 %@" } } } }, "Delete %lld Selected" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Lösche %lld ausgewählte" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar %lld seleccionados" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer %lld sélectionnés" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus %lld yang dipilih" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Elimina %lld selezionati" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%lld 個の選択項目を削除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%lld개 선택 삭제" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń %lld wybranych" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Excluir %lld selecionados" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar %lld Selecionados" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить %lld выбранных" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstrániť %lld vybraných" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izbriši %lld izbranih" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%lld seçili öğeyi sil" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити %lld вибраних" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa %lld đã chọn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "删除 %lld 个已选择项" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "刪除 %lld 個已選取項目" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "刪除 %lld 個已選取項目" } } } }, "Delete %lld Selected Contents" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Lösche %lld ausgewählte Inhalte" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar %lld contenidos seleccionados" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer %lld contenus sélectionnés" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus %lld konten yang dipilih" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Elimina %lld contenuti selezionati" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%lld 個の選択されたコンテンツを削除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%lld개의 선택된 콘텐츠 삭제" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń %lld wybrane treści" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Excluir %lld conteúdos selecionados" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Excluir %lld conteúdos selecionados" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить %lld выбранных содержимого" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstrániť %lld vybrané položky" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izbriši %lld izbranih vsebin" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%lld seçili içeriği sil" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити %lld вибраних вмістів" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa %lld nội dung đã chọn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "删除 %lld 个选定内容" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "刪除 %lld 個選定內容" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "刪除 %lld 個選取內容" } } } }, "Delete %lld Selected Folders" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%lld ausgewählte Ordner löschen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar %lld carpetas seleccionadas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer %lld dossiers sélectionnés" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus %lld folder yang dipilih" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Elimina %lld cartelle selezionate" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%lld 個の選択されたフォルダーを削除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%lld개의 선택된 폴더 삭제" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń %lld wybranych folderów" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Excluir %lld pastas selecionadas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar %lld Pastas Selecionadas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить %lld выбранных папок" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstrániť %lld vybrané priečinky" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izbriši %lld izbranih map" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%lld seçili klasörü sil" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити %lld вибрані папки" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa %lld thư mục đã chọn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "删除 %lld 个选定的文件夹" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "刪除 %lld 個選定的資料夾" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "刪除 %lld 個選定的文件夾" } } } }, "Delete All" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Alle löschen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar todo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Tout supprimer" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus Semua" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Elimina tutto" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "すべて削除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "모두 삭제" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń wszystko" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Excluir Tudo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar Tudo" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить все" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vymazať všetko" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izbriši vse" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hepsini Sil" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити все" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa Tất Cả" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "全部删除" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "全部刪除" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "全部刪除" } } } }, "Delete all files within this folder" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Alle Dateien in diesem Ordner löschen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar todos los archivos dentro de esta carpeta" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer tous les fichiers dans ce dossier" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus semua file dalam folder ini" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Elimina tutti i file in questa cartella" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "このフォルダ内のすべてのファイルを削除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 폴더 내의 모든 파일 삭제" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń wszystkie pliki z tego folderu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Excluir todos os arquivos dentro desta pasta" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar todos os ficheiros nesta pasta" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить все файлы в этой папке" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstrániť všetky súbory v tomto priečinku" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izbriši vse datoteke v tej mapi" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu klasör içindeki tüm dosyaları sil" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити всі файли з цієї папки" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa tất cả tệp trong thư mục này" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "删除此文件夹内所有文件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "刪除此資料夾內的全部檔案" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "刪除此資料夾內的全部檔案" } } } }, "Delete all remaining package files" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Alle verbleibenden Paketdateien löschen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar todos los archivos de paquetes restantes" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer tous les fichiers de package restants" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus semua file paket yang tersisa" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Elimina tutti i file del pacchetto rimanenti" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "すべての残りのパッケージファイルを削除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "남은 패키지 파일 모두 삭제" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń wszystkie pozostałe pliki pakietu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Excluir todos os arquivos de pacotes restantes" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar todos os ficheiros de pacote restantes" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить все оставшиеся файлы пакета" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstrániť všetky zvyšné súbory balíka" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izbriši vse preostale datoteke paketa" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Tüm kalan paket dosyalarını sil" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити всі залишкові файли пакета" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa tất cả các tệp gói còn lại" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "删除所有剩余的包文件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "刪除所有剩餘的包文件" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "刪除所有剩餘套件檔案" } } } }, "Delete Contents" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Inhalte löschen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar contenido" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Suppression de contenu" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus Konten" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Elimina Contenuti" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "内容を削除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "내용 삭제" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń zawartość" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Excluir Conteúdos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar Conteúdos" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить содержимое" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstrániť obsah" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izbriši vsebino" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İçeriği Sil" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити вміст" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa nội dung" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "删除内容" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "刪除內容" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "刪除內容" } } } }, "Delete File" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Datei löschen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar archivo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer le fichier" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus File" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Elimina file" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ファイルを削除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "파일 삭제" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń plik" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Excluir Arquivo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar Ficheiro" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить файл" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstrániť súbor" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izbriši datoteko" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Dosyayı Sil" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити файл" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa Tệp" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "删除文件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "刪除檔案" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "刪除檔案" } } } }, "Delete Folder" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ordner löschen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar carpeta" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Suppression de dossier" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus Folder" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Elimina cartella" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "フォルダを削除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "폴더 삭제" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń folder" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Excluir Pasta" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar Pasta" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить папку" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstrániť priečinok" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izbriši mapo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Klasörü Sil" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити папку" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa thư mục" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "删除文件夹" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "刪除資料夾" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "刪除資料夾" } } } }, "Delete History" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Verlauf löschen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar Historial" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer l'historique" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus Riwayat" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Elimina Cronologia" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "履歴を削除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "기록 삭제" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń historię" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Excluir Histórico" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar Histórico" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить историю" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vymazať históriu" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izbriši zgodovino" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Geçmişi Sil" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити історію" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa Lịch Sử" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "删除历史记录" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "刪除歷史記錄" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "刪除歷史記錄" } } } }, "Delete plugin" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Plugin löschen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar complemento" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer le plugin" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus plugin" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Elimina plugin" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "プラグインを削除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "플러그인 삭제" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń wtyczkę" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Excluir plugin" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar plugin" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить плагин" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstrániť plugin" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izbriši vtičnik" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Eklentiyi sil" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити плагін" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa plugin" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "删除插件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "刪除插件" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "刪除插件" } } } }, "Delete plugin and selected related files" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Plugin und ausgewählte zugehörige Dateien löschen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar plugin y archivos relacionados seleccionados" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer le plugin et les fichiers associés sélectionnés" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus plugin dan file terkait yang dipilih" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Elimina plugin e file correlati selezionati" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "プラグインと選択した関連ファイルを削除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "플러그인 및 선택한 관련 파일 삭제" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń wtyczkę i wybrane powiązane pliki" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Excluir plugin e arquivos relacionados selecionados" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar plugin e ficheiros relacionados selecionados" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить плагин и выбранные связанные файлы" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstrániť plugin a vybrané súvisiace súbory" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izbriši vtičnik in izbrane povezane datoteke" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Eklentiyi ve seçilen ilgili dosyaları sil" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити плагін та вибрані пов'язані файли" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa plugin và các tệp liên quan đã chọn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "删除插件和选定的相关文件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "刪除插件和選定的相關文件" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "刪除插件和選定的相關文件" } } } }, "Delete system-protected files and folders" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Systemgeschützte Dateien und Ordner löschen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar archivos y carpetas protegidos por el sistema" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer les fichiers et dossiers protégés par le système" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus file dan folder yang dilindungi sistem" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Elimina file e cartelle protetti dal sistema" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "システム保護されたファイルとフォルダーを削除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "시스템 보호 파일 및 폴더 삭제" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń pliki i foldery chronione przez system" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Excluir arquivos e pastas protegidos pelo sistema" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar ficheiros e pastas protegidos pelo sistema" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить системно-защищенные файлы и папки" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstrániť systémom chránené súbory a priečinky" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izbriši sistemsko zaščitene datoteke in mape" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sistem korumalı dosya ve klasörleri sil" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити файли та папки, захищені системою" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa các tệp và thư mục được bảo vệ bởi hệ thống" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "删除系统保护的文件和文件夹" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "刪除系統保護的文件和文件夾" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "刪除系統保護的文件和文件夾" } } } }, "Delete the folder" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ordner löschen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar la carpeta" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer le dossier" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus folder" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Elimina la cartella" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "フォルダを削除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "폴더 삭제" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń folder" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Excluir a pasta" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Excluir a pasta" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить папку" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstrániť priečinok" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izbriši mapo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Klasörü sil" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити папку" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa thư mục" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "删除文件夹" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "刪除資料夾" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "刪除資料夾" } } } }, "Delete this file" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Diese Datei löschen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar este archivo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer cd fichier" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus file ini" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Elimina questo file" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "このファイルを削除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 파일 삭제" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń ten plik" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Excluir este arquivo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar este ficheiro" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить этот файл" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstrániť tento súbor" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izbriši to datoteko" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu dosyayı sil" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити цей файл" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa tệp này" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "删除此文件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "刪除此檔案" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "刪除此文件" } } } }, "Delete this schedule" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Diesen Zeitplan löschen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar este horario" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer ce programme" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus jadwal ini" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Elimina questo programma" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "このスケジュールを削除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 일정을 삭제" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń ten harmonogram" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Excluir este cronograma" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar este agendamento" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить это расписание" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vymazať tento rozvrh" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izbriši ta urnik" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu programı sil" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити цей розклад" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa lịch trình này" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "删除此日程" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "刪除此日程" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "刪除此日程" } } } }, "DELETED" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "GELÖSCHT" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "ELIMINADO" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "SUPPRIME(ES)" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "DIHAPUS" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "ELIMINATO" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "削除済み" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "삭제됨" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "USUNIĘTO" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "EXCLUÍDO" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "ELIMINADO" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "УДАЛЕНО" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "ODSTRÁNENÉ" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "IZBRISANO" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "SİLİNDİ" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "ВИДАЛЕНО" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "ĐÃ XÓA" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "已删除" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "已刪除" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "已刪除" } } } }, "Deleted files will appear here" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Gelöschte Dateien werden hier angezeigt" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Los archivos eliminados aparecerán aquí" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Les fichiers supprimés apparaîtront ici" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "File yang dihapus akan muncul di sini" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "I file eliminati appariranno qui" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "削除されたファイルはここに表示されます" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "삭제된 파일이 여기에 나타납니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usunięte pliki pojawią się tutaj" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Os arquivos excluídos aparecerão aqui" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Os ficheiros eliminados aparecerão aqui" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удаленные файлы появятся здесь" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstránené súbory sa zobrazia tu" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izbrisane datoteke se bodo prikazale tukaj" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Silinen dosyalar burada görünecek" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалені файли з'являться тут" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Các tệp đã xóa sẽ xuất hiện ở đây" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "已删除的文件将显示在此处" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "已刪除的檔案將顯示在這裡" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "已刪除的文件會顯示在這裡" } } } }, "Dependencies" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Abhängigkeiten" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Dependencias" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Dépendances" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ketergantungan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Dipendenze" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "依存関係" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "종속성" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zależności" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Dependências" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Dependências" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Зависимости" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Závislosti" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odvisnosti" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bağımlılıklar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Залежності" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Phụ thuộc" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "依赖" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "依賴" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "依賴" } } } }, "DEPRECATED" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "VERALTET" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "OBSOLETO" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "OBSOLETE" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "KADALUARSA" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "DEPRECATO" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "非推奨" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "사용되지 않음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "PRZESTARZAŁE" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "OBSOLETO" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "OBSOLETO" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "УСТАРЕВШЕЕ" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "ZASTARANÉ" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "ZASTARELO" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "KULLANIMDAN KALDIRILMIŞ" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "ЗАСТАРІЛЕ" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "KHÔNG ĐƯỢC KHUYẾN KHÍCH" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "已弃用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "已棄用" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "已棄用" } } } }, "Description" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Beschreibung" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Descripción" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Description" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Deskripsi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Descrizione" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "説明" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "설명" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Opis" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Descrição" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Descrição" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Описание" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Popis" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Opis" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Açıklama" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Опис" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Mô tả" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "描述" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "描述" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "描述" } } } }, "Deselect All" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Alles abwählen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Deseleccionar todo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Tout désélectionner" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Batalkan Pilihan Semua" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Deseleziona tutto" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "すべて選択解除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "모두 선택 해제" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Odznacz wszystko" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Desmarcar todos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Desmarcar Tudo" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Снять выделение со всех" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zrušiť výber všetkého" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Razveljavi izbiro vsega" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Tüm Seçimleri Kaldır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Зняти виділення з усіх" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Bỏ chọn tất cả" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "取消全选" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "取消全選" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "取消全選" } } } }, "Deselect all files in this category" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Alle Dateien in dieser Kategorie abwählen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Deseleccionar todos los archivos en esta categoría" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Désélectionner tous les fichiers de cette catégorie" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Batalkan pilihan semua file dalam kategori ini" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Deseleziona tutti i file in questa categoria" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "このカテゴリのすべてのファイルの選択を解除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 카테고리의 모든 파일 선택 해제" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Odznacz wszystkie pliki w tej kategorii" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Desmarcar todos os arquivos nesta categoria" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Desmarcar todos os ficheiros nesta categoria" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Снять выделение со всех файлов в этой категории" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zrušiť výber všetkých súborov v tejto kategórii" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prekliči izbiro vseh datotek v tej kategoriji" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu kategorideki tüm dosyaların seçimini kaldır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Зняти виділення з усіх файлів у цій категорії" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Bỏ chọn tất cả các tệp trong danh mục này" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "取消选择此类别中的所有文件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "取消選取此類別中的所有檔案" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "取消選取此類別中的所有文件" } } } }, "Details" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Details" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Detalles" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Détails" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Rincian" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Dettagli" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "詳細" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "세부사항" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Szczególy" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Detalhes" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Detalhes" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Подробности" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Podrobnosti" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Podrobnosti" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ayrıntılar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Деталі" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chi tiết" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "详细信息" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "詳情" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "詳情" } } } }, "Detect when apps are moved to Trash" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Erkennen, wenn Anwendungen in den Papierkorb verschoben werden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Detectar cuando las aplicaciones son movidas a la Papelera" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Détecter quand les applications sont déplacées vers la corbeille" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Deteksi saat aplikasi dipindahkan ke Sampah" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Rileva quando le app vengono spostate nel Cestino" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アプリがゴミ箱に移動されたときに検出" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "앱이 휴지통으로 이동될 때 감지" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wykrywanie kiedy aplikacja jest w Koszu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Detectar quando aplicativos são movidos para a Lixeira" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Detectar quando as apps são movidas para o Lixo" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Обнаруживать, когда приложения перемещаются в корзину" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Detekovať presunutie aplikácií do Koša" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Zaznaj, ko so aplikacije premaknjene v koš" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uygulamalar Çöp Sepeti’ne taşındığında algıla" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Відстежувати коли застосунок переміщено в Смітник" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Phát hiện khi ứng dụng được đưa vào Thùng rác" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "检测应用何时被移动到废纸篓" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "偵測應用程式何時丟到垃圾桶" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "偵測應用程式何時丟到垃圾桶" } } } }, "Developer" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Entwickler" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Desarrollador" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Développeur" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pengembang" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Sviluppatore" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "開発者" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "개발자" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Deweloper" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Desenvolvedor" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Desenvolvedor" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Разработчик" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vývojár" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Razvijalec" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Geliştirici" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Розробник" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Nhà phát triển" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "开发者" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "開發者" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "開發者" } } } }, "Development" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Entwicklung" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Desarrollo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Développement" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pengembangan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Sviluppo" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "開発" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "개발" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Rozwój" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Desenvolvimento" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Desenvolvimento" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Разработка" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vývoj" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Razvoj" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Geliştirme" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Розробка" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Nhà phát triển" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "开发" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "開發" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "開發" } } } }, "Development Environments" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Entwicklung Umgebung" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Entornos de Desarrollo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Environnements de développement" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lingkungan Pengembangan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ambienti di sviluppo" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "開発環境" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "개발 환경" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Środowiska programistyczne" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ambientes de Desenvolvimento" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ambientes de Desenvolvimento" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Среды разработки" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vývojové prostredia" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Razvojna okolja" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Geliştirme Ortamları" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Середовища Розробки" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Môi trường phát triển" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "开发环境" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "開發環境" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "開發環境" } } } }, "Disable automatic updates (preserves schedule)" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Automatische Updates deaktivieren (Zeitplan beibehalten)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Desactivar actualizaciones automáticas (mantiene el horario)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Désactiver les mises à jour automatiques (conserve le calendrier)" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Nonaktifkan pembaruan otomatis (mempertahankan jadwal)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Disabilita aggiornamenti automatici (mantiene il programma)" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "自動更新を無効にする(スケジュールを保持)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "자동 업데이트 비활성화 (일정 유지)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyłącz automatyczne aktualizacje (zachowuje harmonogram)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Desativar atualizações automáticas (mantém o cronograma)" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Desativar atualizações automáticas (preserva a programação)" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Отключить автоматические обновления (сохраняет расписание)" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zakázať automatické aktualizácie (zachováva plán)" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Onemogoči samodejne posodobitve (ohrani urnik)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Otomatik güncellemeleri devre dışı bırak (programı korur)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Вимкнути автоматичні оновлення (зберігає розклад)" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tắt cập nhật tự động (giữ nguyên lịch trình)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "禁用自动更新(保留计划)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "停用自動更新(保留計劃)" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "停用自動更新(保留時間表)" } } } }, "Disable pre-releases" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Vorabversionen deaktivieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Desactivar las versiones preliminares" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Désactiver les pré-versions" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Nonaktifkan pra-rilis" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Disabilita le versioni preliminari" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "プレリリースを無効にする" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "사전 릴리즈 비활성화" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyłącz wersje przedpremierowe" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Desativar pré-lançamentos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Desativar pré-lançamentos" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Отключить предварительные релизы" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zakázať predbežné vydania" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Onemogoči predizdaje" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ön sürümleri devre dışı bırak" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Вимкнути попередні випуски" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tắt các bản phát hành trước" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "禁用预发布" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "停用預發布版本" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "禁用預發布" } } } }, "Disabled" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Deaktiviert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Desactivado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Désactivé(es)" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Dinonaktifkan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Disabilitato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "無効" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "비활성화됨" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyłączone" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Desativado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Desativado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Отключено" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zakázané" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Onemogočeno" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Etkin Değil" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Вимкнено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đã tắt" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "关闭" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "已停用" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "已停用" } } } }, "DISABLED" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "DEAKTIVIERT" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "DESACTIVADO" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "DÉSACTIVÉ" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "DINONAKTIFKAN" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "DISABILITATO" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "無効" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "비활성화됨" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "WYŁĄCZONY" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "DESATIVADO" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "DESATIVADO" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "ОТКЛЮЧЕНО" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "DEAKTIVOVANÉ" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "ONEMOGOČENO" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "DEVRE DIŞI" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "ВИМКНЕНО" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "ĐÃ TẮT" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "已禁用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "已停用" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "已停用" } } } }, "Dismiss" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ablehnen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Descartar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Fermer" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tutup" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ignora" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "閉じる" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "무시" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Odrzuć" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Dispensar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Dispensar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Отклонить" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zrušiť" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Opusti" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kapat" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Скасувати" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đóng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "关闭" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "關閉" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "關閉" } } } }, "Download URL" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Download-URL" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "URL de descarga" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "URL de téléchargement" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "URL Unduhan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "URL di download" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ダウンロードURL" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "다운로드 URL" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "URL pobierania" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "URL de download" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "URL de Download" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "URL для загрузки" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "URL na stiahnutie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prenesi URL" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İndirme URL'si" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "URL завантаження" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "URL tải xuống" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "下载网址" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "下載網址" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "下載網址" } } } }, "Drag to expand into grid mode" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ziehen, um in den Rastermodus zu wechseln" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Arrastra para expandir a modo de cuadrícula" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Faites glisser pour passer en mode grille" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Seret untuk memperluas ke mode grid" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Trascina per espandere in modalità griglia" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ドラッグしてグリッドモードに拡大" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "드래그하여 그리드 모드로 확장" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Przeciągnij, aby rozwinąć do trybu siatki" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Arraste para expandir para o modo grade" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Arraste para expandir para o modo de grade" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Перетащите, чтобы развернуть в режим сетки" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Presuňte na rozšírenie do režimu mriežky" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Povleci za razširitev v mrežni način" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Izgara moduna genişletmek için sürükleyin" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Перетягніть, щоб розгорнути в режим сітки" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Kéo để mở rộng thành chế độ lưới" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "拖动以展开为网格模式" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "拖曳以展開為網格模式" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "拖動以展開為網格模式" } } } }, "Drop an app here" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ziehe ein Programm hierhin" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Suelta una app aquí" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Glisser-déposer une application ici" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Letakkan aplikasi di sini" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Trascina un'app qui" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アプリをここにドロップ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "여기에 앱을 드롭하세요" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Upuść aplikację tutaj" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Solte um aplicativo aqui" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Solte um aplicativo aqui" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Перетащите приложение сюда" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Sem presuň aplikáciu" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Spusti aplikacijo tukaj" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Buraya bir uygulama bırakın" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Перемістіть програму сюди" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Kéo một ứng dùng vào đây" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "将应用拖到此处" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "拖放 App 到此處" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "拖放 App 到此處" } } } }, "Drop files or folders above or click to add" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dateien oder Ordner oben ablegen oder zum Hinzufügen anklicken" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Suelta archivos o carpetas arriba o haz clic para añadir" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Glisser-déposer des fichiers ou dossiers ci-dessus ou cliquer pour ajouter" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Jatuhkan file atau folder di atas atau klik untuk menambahkan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Rilascia file o cartelle sopra o clicca per aggiungere" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ファイルやフォルダを上にドロップするか、クリックして追加" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "위에 파일이나 폴더를 드롭하거나 클릭하여 추가" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Upuść pliki lub foldery powyżej lub kliknij, aby dodać" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Solte arquivos ou pastas acima ou clique para adicionar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Solte ficheiros ou pastas acima ou clique para adicionar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Перетащите файлы/папки выше или нажмите, чтобы выбрать" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Presuň súbory alebo priečinky vyššie alebo kliknutím pridaj" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Spustite datoteke ali mape zgoraj ali kliknite za dodajanje" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yukarıya dosya veya klasörleri bırakın veya eklemek için tıklayın" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Перемістити файли чи папки вище або натисніть додати" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Kéo tệp hoặc thư mục vào phía trên hoặc nhấn để thêm" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "将文件或文件夹拖到上方或点击添加" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "拖放檔案或資料夾到上方,或按一下以加入" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "拖放檔案或資料夾到上方,或按一下以加入" } } } }, "Drop folders above or click to add" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ordner oben ablegen oder zum Hinzufügen anklicken" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Suelta carpetas arriba o haz clic para añadir" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Glisser-déposer des dossiers ci-dessus ou cliquez pour ajouter" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Jatuhkan folder di atas atau klik untuk menambahkan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Trascina le cartelle sopra o clicca per aggiungere" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "フォルダを上にドロップするか、クリックして追加" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "위에 폴더를 드롭하거나 클릭하여 추가" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Upuść foldery powyżej lub kliknij, aby dodać" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Arraste pastas acima ou clique para adicionar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Solte as pastas acima ou clique para adicionar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Перетащите папки выше или нажмите, чтобы выбрать" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Presuň priečinky vyššie alebo kliknutím pridaj" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Spustite mape zgoraj ali kliknite za dodajanje" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yukarıya dosyaları bırakın veya eklemek için tıklayın" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Перемістити папки вище або натисніть додати" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Kéo thư mục vào phía trên hoặc nhấn để thêm" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "将文件夹拖到上方或点击添加" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "拖放資料夾到上方,或按一下以加入" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "拖放資料夾到上方,或按一下以加入" } } } }, "Drop Target" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ziel hier ablegen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Destino de Soltar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Glisser-déposer la cible" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Target Letak" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Destinazione di rilascio" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ドロップターゲット" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "드롭 대상" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Upuść cel" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Alvo Soltar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Alvo de Soltar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Сбросить цель" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Cieľ presunutia" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Cilj spusta" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hedefi Bırak" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Скинути ціль" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Kéo thả ứng dụng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "拖放目标" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "拖放目標" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "拖放目標" } } } }, "e.g., firefox" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "z. B. Firefox" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "p. ej., Firefox" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "par ex., Firefox" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "misalnya, Firefox" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "ad es. Firefox" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "例: Firefox" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "예: Firefox" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "np. firefox" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "por exemplo, Firefox" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "por exemplo, firefox" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "например, Firefox" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "napr. firefox" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "npr. Firefox" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "örn. Firefox" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "наприклад, firefox" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "ví dụ: Firefox" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "例如,Firefox" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "例如,Firefox" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "例如,Firefox" } } } }, "Elapsed Time: %@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Verstrichene Zeit: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Tiempo transcurrido: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Temps écoulé : %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Waktu berlalu: %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Tempo trascorso: %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "経過時間: %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "경과 시간: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Czas, który upłynął: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Tempo decorrido: %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Tempo Decorrido: %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Прошедшее время: %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Uplynutý čas: %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pretečeni čas: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Geçen Süre: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Минувший час: %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thời gian đã trôi qua: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "已用时间:%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "經過時間:%@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "經過時間:%@" } } } }, "Enable automatic updates" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Automatische Updates aktivieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Habilitar actualizaciones automáticas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Activer les mises à jour automatiques" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Aktifkan pembaruan otomatis" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Abilita aggiornamenti automatici" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "自動更新を有効にする" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "자동 업데이트 활성화" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Włącz automatyczne aktualizacje" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ativar atualizações automáticas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ativar atualizações automáticas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Включить автоматические обновления" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Povoliť automatické aktualizácie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Omogoči samodejne posodobitve" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Otomatik güncellemeleri etkinleştir" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Увімкнути автоматичні оновлення" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Bật cập nhật tự động" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "启用自动更新" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "啟用自動更新" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "啟用自動更新" } } } }, "Enable context menu extension for Finder" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Kontextmenü-Erweiterung für Finder aktivieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Habilitar la extensión del menú contextual para Finder" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Activer l'extension du menu contextuel pour le Finder" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Aktifkan ekstensi menu konteks untuk Finder" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Abilita l'estensione del menu contestuale per Finder" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Finder用のコンテキストメニュー拡張を有効にする" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Finder의 컨텍스트 메뉴 확장 활성화" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Włącz rozszerzenie menu kontekstowego dla Finder" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ativar extensão de menu de contexto para o Finder" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ativar extensão do menu de contexto para o Finder" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Включить расширение для контекстного меню Finder" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zapnúť rozšírenie kontextového menu pre Finder" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Omogoči razširitev kontekstnega menija za Finder" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Finder için bağlamsal menü uzantısını etkinleştir" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Увімкнути розширення для контекстного меню Finder" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Bật tiện ích menu ngữ cảnh cho Finder" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "为Finder启用上下文菜单扩展" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "為 Finder 啟用特色選單延伸功能" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "為 Finder 啟用特色選單延伸功能" } } } }, "Enable Helper" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Helfer aktivieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Habilitar Asistente" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Activer l'assistant" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Aktifkan Pembantu" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Abilita Assistente" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ヘルパーを有効にする" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "도우미 활성화" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Włącz Pomocnika" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ativar Assistente" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ativar Assistente" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Включить помощника" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Povoliť Pomocníka" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Omogoči Pomočnika" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yardımcıyı Etkinleştir" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Увімкнути Помічника" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Bật Trợ lý" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "启用助手" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "啟用助手" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "啟用助手" } } } }, "Enable icon for Finder extension" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Symbol für Finder-Erweiterung aktivieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Habilitar icono para la extensión de Finder" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Activer l'icône pour l'extension Finder" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Aktifkan ikon untuk ekstensi Finder" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Abilita icona per l'estensione di Finder" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Finder拡張機能のアイコンを有効にする" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Finder 확장에 아이콘 활성화" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Włącz ikonę dla rozszerzenia Finder" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ativar ícone para extensão do Finder" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ativar ícone para extensão do Finder" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Включить значок для расширения Finder" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Povoliť ikonu pre rozšírenie Finder" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Omogoči ikono za razširitev Finder" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Finder uzantısı için simgeyi etkinleştir" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Увімкнути значок для розширення Finder" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Bật biểu tượng cho tiện ích mở rộng Finder" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "启用 Finder 扩展图标" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "啟用 Finder 擴充功能圖示" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "啟用 Finder 擴充功能圖示" } } } }, "Enable pre-releases" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Vorabversionen aktivieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Habilitar versiones preliminares" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Activer les pré-versions" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Aktifkan pra-rilis" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Abilita le versioni preliminari" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "プレリリースを有効にする" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "사전 릴리스 활성화" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Włącz wersje przedpremierowe" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Habilitar pré-lançamentos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ativar pré-lançamentos" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Включить предварительные релизы" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Povoliť predbežné verzie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Omogoči predizdaje" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ön sürümleri etkinleştir" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Увімкнути попередні версії" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Bật các phiên bản phát hành trước" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "启用预发布" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "啟用預發布" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "啟用預發布" } } } }, "Enable verbose logging and bundle cache flushing for troubleshooting" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Aktivieren Sie ausführliches Logging und das Leeren des Bundle-Caches zur Fehlerbehebung" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Habilitar registro detallado y vaciado de caché de paquetes para solución de problemas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Activer la journalisation détaillée et le vidage du cache de paquet pour le dépannage" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Aktifkan pencatatan rinci dan penyegaran cache bundel untuk pemecahan masalah" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Abilita la registrazione dettagliata e lo svuotamento della cache dei pacchetti per la risoluzione dei problemi" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "トラブルシューティングのために詳細なログ記録とバンドルキャッシュのフラッシュを有効にする" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "문제 해결을 위해 자세한 로깅 및 번들 캐시 제거 활성화" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Włącz szczegółowe logowanie i czyszczenie pamięci podręcznej pakietów w celu rozwiązywania problemów" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ativar log detalhado e limpeza de cache de pacotes para solução de problemas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ativar registo detalhado e limpeza da cache de pacotes para resolução de problemas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Включить подробное ведение журнала и очистку кеша пакетов для устранения неполадок" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Povoliť podrobné protokolovanie a vyprázdnenie vyrovnávacej pamäte balíka na riešenie problémov" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Omogočite podrobno beleženje in izpraznitev predpomnilnika paketa za odpravljanje težav" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sorun gidermek için ayrıntılı günlük kaydını ve paket önbellek temizlemeyi etkinleştir" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Увімкнути детальне журналювання та очищення кешу пакетів для усунення несправностей" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Bật ghi nhật ký chi tiết và xóa bộ nhớ đệm gói để khắc phục sự cố" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "启用详细日志记录和捆绑缓存刷新以进行故障排除" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "啟用詳細日誌記錄和捆綁緩存刷新以進行故障排除" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "啟用詳細日誌記錄和捆綁緩存刷新以進行故障排除" } } } }, "Enabled" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Aktiviert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Habilitado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Activé(es)" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Diaktifkan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Abilitato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "有効" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "활성화" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Włączone" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Habilitado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ativado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Включено" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Povolené" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Omogočeno" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Etkin" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Увімкнено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đã bật" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "开启" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "已啟用" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "已啟用" } } } }, "Enabling the CLI will allow you to execute Pearcleaner actions from the Terminal. This will add pearcleaner command into /usr/local/bin so it's available directly from your PATH environment variable. Try it after enabling:\n\n> pear --help" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Wenn Sie die CLI aktivieren, können Sie Pearcleaner-Aktionen über das Terminal ausführen. Dies fügt den Befehl pearcleaner in /usr/local/bin ein, so dass er direkt über Ihre PATH-Umgebungsvariable verfügbar ist. Probieren Sie es nach der Aktivierung aus:\n\n> pear —Hilfe" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Habilitar la CLI te permitirá ejecutar acciones de Pearcleaner desde el Terminal. Esto añadirá el comando pearcleaner en /usr/local/bin para que esté disponible directamente desde tu variable de entorno PATH. Inténtalo después de habilitar:\n\n> pear --help" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ceci active l’option d’interface de ligne de commande vous permettant d'exécuter des actions liées à Pearcleaner à partir du terminal de macOS en ajoutant la commande \"pearcleaner\" dans /usr/local/bin afin qu'elle soit disponible directement à partir de votre variable d'environnement PATH. Après avoir activer cette option, essayez par vous même en tapant \"pear --help\" dans le terminal." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Mengaktifkan CLI/Baris Perintah akan memungkinkan Anda menjalankan tindakan Pearcleaner dari Terminal. Ini akan menambahkan perintah pearcleaner ke dalam /usr/local/bin sehingga tersedia langsung dari variabel lingkungan PATH Anda. Coba perintah di bawah ini setelah mengaktifkan:\n\n> pear --help" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Abilitare la CLI ti permetterà di eseguire azioni di Pearcleaner dal Terminale. Questo aggiungerà il comando pearcleaner in /usr/local/bin rendendolo disponibile direttamente dalla tua variabile d'ambiente PATH. Provalo dopo averlo abilitato:\n\n> pear --help" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "CLIを有効にすると、ターミナルからPearcleanerのアクションを実行できます。これにより、pearcleanerコマンドが/usr/local/binに追加され、PATH環境変数から直接使用できるようになります。有効にした後、以下を試してください:\n\n> pear --help" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Pearclaner CLI를 활성화하면 터미널에서 Pearcleaner 작업을 실행할 수 있습니다. 이는 pearcleaner 명령을 /usr/local/bin에 추가하여 PATH 환경 변수에서 직접 사용할 수 있게 합니다. 활성화 후 시도해 보세요:\n\n> pear --help" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Włączenie CLI umożliwi wykonywanie działań Pearcleaner z poziomu terminala. Spowoduje to dodanie polecenia pearcleaner do katalogu /usr/local/bin, dzięki czemu będzie ono dostępne bezpośrednio z poziomu zmiennej środowiskowej PATH. Po włączeniu spróbuj wykonać polecenie:\n\n> pear --help" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Habilitar a CLI permitirá que você execute ações do Pearcleaner a partir do Terminal. Isso adicionará o comando pearcleaner em /usr/local/bin, tornando-o disponível diretamente a partir da sua variável de ambiente PATH. Experimente depois de habilitar:\n\n> pear --help" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ativar a CLI permitirá que execute ações do Pearcleaner a partir do Terminal. Isto adicionará o comando pearcleaner em /usr/local/bin para que esteja disponível diretamente a partir da sua variável de ambiente PATH. Experimente após ativar:\n\n> pear --help" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Включение CLI позволит вам выполнять действия с Pearcleaner из терминала. Это добавит команду pearcleaner в /usr/local/bin, чтобы она была доступна непосредственно из вашей переменной окружения PATH. Попробуйте ее после включения:\n\n> pear —help" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zapnutím CLI budeš môcť vykonávať akcie Pearcleaneru z terminálu. Tým sa pridá príkaz pearcleaner do priečinka /usr/local/bin, takže bude dostupný priamo z tvojej premennej prostredia PATH. Skús to po zapnutí:\n\n> pear --help" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Omogočanje CLI vam bo omogočilo izvajanje dejanj Pearcleaner iz terminala. To bo dodalo ukaz pearcleaner v /usr/local/bin, tako da bo neposredno na voljo iz vaše spremenljivke okolja PATH. Poskusite po omogočitvi:\n\n> pear --help" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "CLI’yı etkinleştirmek Pearcleaner işlevlerini Terminal’den gerçekleştirmenize imkan sağlar. Bu, pearcleaner komutunu PATH ortam değişkeninizden direkt olarak ulaşılabilir olması için /usr/local/bin içine ekleyecektir. Etkinleştirdikten sonra deneyin:\n\n> pear --help" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Увімкнення CLI дозволить вам виконувати дії Pearcleaner з терміналу. Це додасть команду pearcleaner до /usr/local/bin, щоб вона була доступна безпосередньо з вашої змінної оточення PATH. Спробуйте після увімкнення:\n\n> pear --help" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Bật sử dụng dòng lệnh (CLI) sẽ cho phép thực thi các hành động của Pearcleaner từ Terminal. Việc này sẽ thêm lệnh pearcleaner vào thư mục /usr/local/bin, có thể gọi lệnh trực tiếp thông qua biến môi trường (PATH). Hãy thử lệnh này sau khi bật:\n\n> pear --help" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "启用 CLI 后,您可以从终端执行 Pearcleaner 操作。这将把 pearcleaner 命令添加到 /usr/local/bin 中,使其可以直接从 PATH 环境变量中访问。启用后尝试:\n\n> pear --help" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "啟用命令列介面(CLI)會允許你透過終端機執行 Pearcleaner 動作。這樣會將 pearcleaner 命令加至 /usr/local/bin 下,以便直接從你的 PATH 環境變數使用。當你啟用之後,就可以嘗試它:\n\n> pear --help" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "啟用命令列介面(CLI)會允許你透過終端機執行 Pearcleaner 動作。這樣會將 pearcleaner 命令加至 /usr/local/bin 下,以便直接從你的 PATH 環境變數使用。當你啟用之後,就可以嘗試它:\n\n> pear --help" } } } }, "Enabling this extension will allow you to right click apps in Finder to quickly uninstall them with Pearcleaner" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Wenn Sie diese Erweiterung aktivieren, können Sie Anwendungen im Finder mit der rechten Maustaste anklicken, um sie mit Pearcleaner zu deinstallieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Habilitar esta extensión te permitirá hacer clic derecho en las aplicaciones en Finder para desinstalarlas rápidamente con Pearcleaner" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Activer cette extension vous permettra de désinstaller rapidement une application avec Pearcleaner dans le Finder en faisant un clic droit dessus." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Mengaktifkan ekstensi ini akan memungkinkan Anda mengklik kanan aplikasi di Finder untuk cepat mencopot pemasangannya dengan Pearcleaner" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Abilitando questa estensione, potrai fare clic con il tasto destro sulle app in Finder per disinstallarle rapidamente con Pearcleaner." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "この拡張機能を有効にすると、Finderでアプリを右クリックしてPearcleanerですぐにアンインストールできます" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 확장을 활성화하면 Finder에서 앱을 마우스 오른쪽 버튼으로 클릭하여 Pearcleaner로 빠르게 제거할 수 있습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Włączenie tego rozszerzenia pozwoli Ci kliknąć prawym przyciskiem myszy na aplikacje w Finderze, aby szybko je odinstalować za pomocą Pearcleaner" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ativar esta extensão permitirá que você clique com o botão direito nos aplicativos no Finder para desinstalá-los rapidamente com o Pearcleaner" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ativar esta extensão permitirá clicar com o botão direito nos aplicativos no Finder para desinstalá-los rapidamente com o Pearcleaner" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Включение этого расширения позволит вам щелкать правой кнопкой мыши приложения в Finder, чтобы быстро удалить их с помощью Pearcleaner" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zapnutím tohto rozšírenia budeš môcť kliknúť pravým tlačidlom myši na aplikácie vo Finderi a rýchlo ich odinštalovať pomocou Pearcleaneru." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Omogočanje te razširitve vam bo omogočilo, da z desnim klikom na aplikacije v Finderju hitro odstranite z Pearcleanerjem" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu uzantıyı etkinleştirmek Finder’da uygulamalara sağ tıklayarak Pearcleaner’la hızlıca kaldırmanıza imkan sağlar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Увімкнувши це розширення, ви зможете клацнути правою кнопкою миші на програмах у Finder, щоб швидко видалити їх за допомогою Pearcleaner" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Bật tiện ích mở rộng này sẽ cho phép bạn nhấp chuột phải vào các ứng dụng trong Finder để gỡ cài đặt nhanh chóng chúng bằng Pearcleaner." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "启用此扩展后,您可以在 Finder 中右键点击应用,使用 Pearcleaner 快速卸载它们" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "啟用此延伸功能可讓你在 Finder 中右鍵點按一下應用程式,以使用 Pearcleaner 快速解除安裝" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "啟用此延伸功能可讓你在 Finder 中右鍵點按一下應用程式,以使用 Pearcleaner 快速解除安裝" } } } }, "End Date" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Enddatum" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Fecha de finalización" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Date de fin" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tanggal Akhir" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Data di fine" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "終了日" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "종료 날짜" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Data zakończenia" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Data de Término" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Data de Término" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Дата окончания" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Dátum ukončenia" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Končni datum" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bitiş Tarihi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Дата завершення" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ngày kết thúc" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "结束日期" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "結束日期" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "結束日期" } } } }, "Enhanced" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Verbessert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Mejorado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Amélioré" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ditingkatkan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Migliorato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "強化" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "항샹" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ulepszony" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Aprimorado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Aprimorado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Улучшено" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vylepšené" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izboljšano" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Geliştirilmiş" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Покращено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Nâng cao" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "增强" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "增強" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "增強" } } } }, "Enter exactly 5 hex colors separated by commas in the order above" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Geben Sie genau 5 Hex-Farben ein, die durch Kommas in der oben genannten Reihenfolge getrennt sind" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Introduce exactamente 5 colores hexadecimales separados por comas en el orden indicado arriba" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Saisissez 5 couleurs hexadécimales séparées par des virgules dans l'ordre ci-dessus" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Masukkan tepat 5 warna hex yang dipisahkan dengan koma sesuai urutan di atas" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Inserisci esattamente 5 colori esadecimali separati da virgole nell'ordine sopra" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "上記の順序でコンマで区切って、ちょうど 5 つの 16 進数カラーを入力してください" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "위의 순서대로 정확히 쉼표로 구분하여 5개의 16진수 색상을 입력해주세요" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wprowadź dokładnie 5 kolorów szesnastkowych (HEX) oddzielonych przecinkami w powyższej kolejności" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Digite exatamente 5 cores hexadecimais separadas por vírgulas na ordem acima" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Insira exatamente 5 cores hexadecimais separadas por vírgulas na ordem acima" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Введите ровно 5 шестнадцатеричных цветов, разделенных запятыми, в указанном выше порядке" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zadajte presne 5 hex farieb oddelených čiarkami v poradí vyššie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Vnesite natanko 5 šestnajstiških barv, ločenih z vejicami, v zgornjem vrstnem redu" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yukarıdaki sırayla virgülle ayrılmış tam olarak 5 hex rengi girin" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Введіть рівно 5 шістнадцяткових кольорів, розділених комами, у зазначеному вище порядку" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Nhập chính xác 5 mã màu hex được ngăn cách bằng dấu phẩy theo thứ tự trên" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "按上述顺序输入由逗号分隔的 5 个十六进制颜色代码" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "請按上述順序輸入剛好五個用逗號分隔的十六進位顏色" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "請按照上面的順序輸入 5 個以逗號分隔的十六進位顏色 " } } } }, "Enter manual command here, Enter to run" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Geben Sie hier manuelle Befehle ein, drücken Sie die Eingabetaste, um diese auszuführen." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ingrese comando manual aquí, Enter para ejecutar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Entrez la commande manuelle ici, Entrée pour exécuter" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Masukkan perintah manual di sini, tekan Enter untuk menjalankan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Inserisci qui il comando manuale, premi Invio per eseguire" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ここに手動コマンドを入力し、Enter キーを押して実行" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "여기에 명령 입력, 실행하려면 Enter" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wpisz polecenie ręczne, naciśnij Enter, aby uruchomić" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Digite o comando manual aqui, Enter para executar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Introduza o comando manual aqui, Enter para executar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Введите команду вручную, нажмите Enter для выполнения" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zadajte príkaz ručne, stlačením Enter ho spustíte" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Tukaj vnesite ročni ukaz, Enter za zagon" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Buraya manuel komutu girin, çalıştırmak için Enter'a basın" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Введіть ручну команду тут, Enter для запуску" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Nhập lệnh thủ công ở đây, nhấn Enter để chạy" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "在此处输入手动命令,按回车键运行" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "在此處輸入手動命令,按一下 Enter 以執行" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "在此處輸入手動命令,按一下 Enter 以執行" } } } }, "Enter the tap name to add" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Geben Sie den Namen des Wasserhahns ein, um ihn hinzuzufügen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ingrese el nombre del grifo para agregar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Entrez le nom du robinet à ajouter" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Masukkan nama keran untuk ditambahkan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Inserisci il nome del tap da aggiungere" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "追加するタップ名を入力してください" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "추가할 탭 이름 입력" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wpisz nazwę kranu, aby dodać" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Digite o nome da torneira para adicionar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Introduza o nome do tap a adicionar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Введите название крана для добавления" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zadajte názov kohútika na pridanie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Vnesite ime pipe za dodajanje" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Eklemek için musluk adını girin" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Введіть назву крана для додавання" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Nhập tên vòi để thêm" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "输入要添加的水龙头名称" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "輸入要添加的水龍頭名稱" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "輸入要添加的水龍頭名稱" } } } }, "Error" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Fehler" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Error" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Erreur" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Kesalahan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Errore" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "エラー" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "오류" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Błąd" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Erro" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Erro" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Ошибка" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Chyba" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Napaka" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hata" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Помилка" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Lỗi" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "错误" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "錯誤" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "錯誤" } } } }, "Everything in Enhanced plus adds company name and team identifier. Searches file contents, metadata, Finder comments, and files created by the app. Most comprehensive cleanup, finds all resources associated with the app and the developer, even other apps they create." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Alles in Enhanced plus fügt den Firmennamen und die Teamkennung hinzu. Durchsucht Dateiinhalte, Metadaten, Finder-Kommentare und von der App erstellte Dateien. Umfassendste Bereinigung, findet alle Ressourcen, die mit der App und dem Entwickler verbunden sind, sogar andere Apps, die sie erstellen." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Todo en Enhanced más añade el nombre de la empresa y el identificador del equipo. Busca el contenido de los archivos, los metadatos, los comentarios de Finder y los archivos creados por la aplicación. La limpieza más completa, encuentra todos los recursos asociados con la aplicación y el desarrollador, incluso otras aplicaciones que crean." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Tout dans Enhanced plus ajoute le nom de l'entreprise et l'identifiant de l'équipe. Recherche le contenu des fichiers, les métadonnées, les commentaires du Finder et les fichiers créés par l'application. Nettoyage le plus complet, trouve toutes les ressources associées à l'application et au développeur, même d'autres applications qu'ils créent." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Semua dalam Enhanced plus menambahkan nama perusahaan dan pengenal tim. Mencari isi file, metadata, komentar Finder, dan file yang dibuat oleh aplikasi. Pembersihan paling komprehensif, menemukan semua sumber daya yang terkait dengan aplikasi dan pengembang, bahkan aplikasi lain yang mereka buat." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Tutto in Enhanced più aggiunge il nome dell'azienda e l'identificatore del team. Cerca il contenuto dei file, i metadati, i commenti di Finder e i file creati dall'app. La pulizia più completa, trova tutte le risorse associate all'app e allo sviluppatore, anche altre app che creano." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Enhancedのすべてに加え、会社名とチーム識別子を追加します。ファイルの内容、メタデータ、Finderコメント、アプリで作成されたファイルを検索します。最も包括的なクリーンアップで、アプリと開発者に関連するすべてのリソースを見つけ、彼らが作成する他のアプリも見つけます。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Enhanced의 모든 기능에 회사 이름과 팀 식별자를 추가합니다. 파일 내용, 메타데이터, Finder 각주 및 앱에서 생성한 파일을 검색합니다. 가장 포괄적인 정리로, 앱 및 개발자와 관련된 모든 리소스를 찾고, 그들이 만든 다른 앱들도 찾습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wszystko w wersji Enhanced plus dodaje nazwę firmy i identyfikator zespołu. Przeszukuje zawartość plików, metadane, komentarze Findera i pliki utworzone przez aplikację. Najbardziej kompleksowe czyszczenie, znajduje wszystkie zasoby związane z aplikacją i deweloperem, nawet inne aplikacje, które tworzą." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Tudo no Enhanced plus adiciona o nome da empresa e o identificador da equipe. Pesquisa o conteúdo dos arquivos, metadados, comentários do Finder e arquivos criados pelo aplicativo. A limpeza mais abrangente, encontra todos os recursos associados ao aplicativo e ao desenvolvedor, até mesmo outros aplicativos que eles criam." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Tudo no Enhanced mais adiciona o nome da empresa e o identificador da equipe. Pesquisa conteúdos de arquivos, metadados, comentários do Finder e arquivos criados pelo aplicativo. Limpeza mais abrangente, encontra todos os recursos associados ao aplicativo e ao desenvolvedor, até mesmo outros aplicativos que eles criam." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Всё в Enhanced плюс добавляет название компании и идентификатор команды. Ищет содержимое файлов, метаданные, комментарии Finder и файлы, созданные приложением. Самая полная очистка, находит все ресурсы, связанные с приложением и разработчиком, даже другие приложения, которые они создают." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Všetko v Enhanced plus pridáva názov spoločnosti a identifikátor tímu. Vyhľadáva obsah súborov, metadáta, komentáre Finderu a súbory vytvorené aplikáciou. Najkomplexnejšie čistenie, nájde všetky zdroje spojené s aplikáciou a vývojárom, dokonca aj iné aplikácie, ktoré vytvárajú." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Vse v Enhanced plus doda ime podjetja in identifikator ekipe. Išče vsebino datotek, metapodatke, komentarje Finderja in datoteke, ustvarjene z aplikacijo. Najbolj celovito čiščenje, najde vse vire, povezane z aplikacijo in razvijalcem, tudi druge aplikacije, ki jih ustvarijo." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Enhanced'daki her şeyin yanı sıra şirket adı ve ekip tanımlayıcısı ekler. Dosya içeriklerini, meta verileri, Finder yorumlarını ve uygulama tarafından oluşturulan dosyaları arar. En kapsamlı temizlik, uygulama ve geliştirici ile ilişkili tüm kaynakları bulur, hatta onların oluşturduğu diğer uygulamaları bile bulur." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Усе в Enhanced плюс додає назву компанії та ідентифікатор команди. Шукає вміст файлів, метадані, коментарі Finder та файли, створені додатком. Найбільш комплексне очищення, знаходить усі ресурси, пов’язані з додатком та розробником, навіть інші додатки, які вони створюють." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Mọi thứ trong Enhanced cộng thêm tên công ty và mã định danh nhóm. Tìm kiếm nội dung tệp, siêu dữ liệu, nhận xét Finder và các tệp được tạo bởi ứng dụng. Dọn dẹp toàn diện nhất, tìm thấy tất cả các tài nguyên liên quan đến ứng dụng và nhà phát triển, thậm chí cả các ứng dụng khác mà họ tạo ra." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "增强版中的所有功能加上公司名称和团队标识符。搜索文件内容、元数据、Finder评论和应用程序创建的文件。最全面的清理,找到与应用程序和开发者相关的所有资源,甚至是他们创建的其他应用程序。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "增強版中的所有功能加上公司名稱和團隊識別符。搜索文件內容、元數據、Finder評論和應用程序創建的文件。最全面的清理,找到與應用程序和開發者相關的所有資源,甚至是他們創建的其他應用程序。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "增強版中的所有內容還添加了公司名稱和團隊標識符。搜索文件內容、元數據、Finder 評論和應用程序創建的文件。最全面的清理,找到與應用程序和開發人員相關的所有資源,甚至是他們創建的其他應用程序。" } } } }, "Everything in Strict plus partial string matches. May find a few unrelated files in some cases." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Alles im strengen Modus plus teilweise Zeichenfolgenübereinstimmungen. In einigen Fällen können einige nicht zusammenhängende Dateien gefunden werden." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Todo en modo estricto más coincidencias parciales de cadenas. En algunos casos, puede encontrar algunos archivos no relacionados." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Tout en mode strict plus correspondances partielles de chaînes. Dans certains cas, il peut trouver quelques fichiers non liés." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Segala sesuatu dalam mode ketat ditambah kecocokan string parsial. Dalam beberapa kasus, mungkin menemukan beberapa file yang tidak terkait." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Tutto in modalità rigorosa più corrispondenze parziali di stringhe. In alcuni casi potrebbe trovare alcuni file non correlati." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "厳密モードのすべてに加えて部分的な文字列一致。場合によっては、いくつかの無関係なファイルが見つかることがあります。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "엄격 모드의 모든 것과 부분 문자열 일치. 경우에 따라 관련 없는 파일이 몇 개 발견될 수 있습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wszystko w trybie ścisłym plus częściowe dopasowania ciągów. W niektórych przypadkach może znaleźć kilka niepowiązanych plików." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Tudo no modo Estrito mais correspondências parciais de strings. Em alguns casos, pode encontrar alguns arquivos não relacionados." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Tudo no modo Estrito mais correspondências parciais de cadeias. Pode encontrar alguns ficheiros não relacionados em alguns casos." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Все в строгом режиме плюс частичные совпадения строк. В некоторых случаях могут быть найдены несколько несвязанных файлов." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Všetko v prísnom režime plus čiastočné zhody reťazcov. V niektorých prípadoch môže nájsť niekoľko nesúvisiacich súborov." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Vse v strogem načinu plus delna ujemanja nizov. V nekaterih primerih lahko najdete nekaj nepovezanih datotek." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Katı modda her şey ve kısmi dize eşleşmeleri. Bazı durumlarda birkaç alakasız dosya bulunabilir." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Усе в режимі суворого плюс часткові збіги рядків. У деяких випадках може знайти кілька непов'язаних файлів." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Mọi thứ trong chế độ nghiêm ngặt cộng với các kết quả khớp chuỗi một phần. Trong một số trường hợp có thể tìm thấy một vài tệp không liên quan." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "严格模式中的所有内容加上部分字符串匹配。在某些情况下,可能会找到一些不相关的文件。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "嚴格模式中的所有內容加上部分字串匹配。在某些情況下,可能會找到一些不相關的文件。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "嚴格模式中的所有內容加上部分字符串匹配。在某些情況下,可能會找到一些不相關的文件。" } } } }, "Exact string matches only for app name, bundle ID, and entitlements. Most conservative, recommended as default choice." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Exakte Zeichenfolgenübereinstimmungen nur für App-Namen, Bundle-ID und Berechtigungen. Am konservativsten, empfohlen als Standardwahl." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Coincidencias exactas de cadenas solo para el nombre de la aplicación, ID del paquete y permisos. Más conservador, recomendado como opción predeterminada." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Correspondances exactes de chaînes uniquement pour le nom de l'application, l'ID du bundle et les droits. Plus conservateur, recommandé comme choix par défaut." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hanya pencocokan string yang tepat untuk nama aplikasi, ID bundel, dan hak. Paling konservatif, direkomendasikan sebagai pilihan default." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Corrispondenze esatte di stringhe solo per nome dell'app, ID bundle e autorizzazioni. Più conservativo, consigliato come scelta predefinita." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アプリ名、バンドルID、および権限に対してのみ正確な文字列一致。最も保守的で、デフォルトの選択として推奨されます。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "앱 이름, 번들 ID 및 권한에 대한 정확한 문자열 일치만. 가장 보수적이며 기본 선택으로 권장됩니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dokładne dopasowanie ciągu znaków tylko dla nazwy aplikacji, identyfikatora pakietu i uprawnień. Najbardziej konserwatywne, zalecane jako domyślny wybór." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Correspondências exatas de strings apenas para nome do aplicativo, ID do pacote e permissões. Mais conservador, recomendado como escolha padrão." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Correspondências exatas de strings apenas para nome da app, ID do pacote e direitos. Mais conservador, recomendado como escolha padrão." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Точные совпадения строк только для имени приложения, идентификатора пакета и прав. Самый консервативный, рекомендуется как выбор по умолчанию." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Presné zhody reťazcov iba pre názov aplikácie, ID balíka a oprávnenia. Najkonzervatívnejšie, odporúčané ako predvolená voľba." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Natančna ujemanja nizov samo za ime aplikacije, ID paketa in pravice. Najbolj konzervativna, priporočena kot privzeta izbira." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yalnızca uygulama adı, paket kimliği ve yetkiler için tam dize eşleşmeleri. En muhafazakar, varsayılan seçim olarak önerilir." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Точні збіги рядків лише для назви програми, ідентифікатора пакета та прав. Найбільш консервативний, рекомендований як вибір за замовчуванням." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chỉ khớp chuỗi chính xác cho tên ứng dụng, ID gói và quyền. Bảo thủ nhất, được khuyến nghị là lựa chọn mặc định." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "仅适用于应用名称、包 ID 和权限的精确字符串匹配。最保守,建议作为默认选择。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "僅精確匹配應用程式名稱、包ID和權限。最保守,建議作為默認選擇。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "僅適用於應用程式名稱、套件 ID 和權限的精確字串匹配。最保守,建議作為預設選擇。" } } } }, "Example privileged commands" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Beispiel für privilegierte Befehle" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ejemplo de comandos privilegiados" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Exemples de commandes avec priviléges" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Contoh perintah istimewa" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Esempio di comandi privilegiati" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "特権コマンドの例" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "권한 상승 예제 명령어" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Przykładowe polecenia uprzywilejowane" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Exemplo de comandos privilegiados" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Exemplo de comandos privilegiados" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Примеры привилегированных команд" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Príklady privilegovaných príkazov" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Primer privilegiranih ukazov" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Örnek ayrıcalıklı komutlar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Приклади привілейованих команд" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ví dụ về các lệnh yêu cầu quyền cao" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "特权命令示例" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "特殊權限命令範例" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "特殊權限命令範例" } } } }, "Examples:" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Beispiele:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ejemplos:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Exemples :" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Contoh:" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Esempi:" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "例:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "예시:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Przykłady:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Exemplos:" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Exemplos:" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Примеры:" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Príklady:" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Primeri:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Örnekler:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Приклади:" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ví dụ:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "例子:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "範例:" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "例子:" } } } }, "Exclude" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ausschliessen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Excluir" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Exclure" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Kecualikan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Escludi" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "除外" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "제외" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyklucz" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Excluir" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Excluir" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Исключить" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vylúčiť" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izključi" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hariç Tut" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Виключити" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Loại trừ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "排除" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "排除" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "排除" } } } }, "Exclude %lld Selected" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ausgewählte %lld ausschließen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Excluir %lld seleccionados" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Exclure %lld sélectionnés" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Kecualikan %lld yang dipilih" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Escludi %lld selezionati" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%lld 個の選択を除外" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%lld개 선택 제외" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyklucz %lld wybranych" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Excluir %lld selecionados" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Excluir %lld Selecionados" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Исключить %lld выбранных" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vylúčiť %lld vybraných" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izključi %lld izbranih" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%lld seçiliyi hariç tut" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Виключити %lld вибраних" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Loại trừ %lld đã chọn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "排除 %lld 个已选择" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "排除 %lld 個已選取" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "排除 %lld 個已選取" } } } }, "Exclude from app file search" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Von der App-Dateisuche ausschließen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Excluir de la búsqueda de archivos de la aplicación" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Exclure de la recherche de fichiers de l'application" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Kecualikan dari pencarian file aplikasi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Escludi dalla ricerca file dell'app" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アプリのファイル検索から除外" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "앱 파일 검색에서 제외" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyklucz z wyszukiwania plików aplikacji" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Excluir da pesquisa de arquivos do aplicativo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Excluir da pesquisa de ficheiros da aplicação" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Исключить из поиска файлов приложения" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vylúčiť z vyhľadávania súborov aplikácie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izključi iz iskanja datotek aplikacije" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uygulama dosyası aramasından hariç tut" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Виключити з пошуку файлів додатка" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Loại trừ khỏi tìm kiếm tệp ứng dụng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "从应用文件搜索中排除" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "從應用程式檔案搜尋中排除" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "從應用程式檔案搜尋中排除" } } } }, "Exclude from orphaned file search" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Von der Suche nach verwaisten Dateien ausschließen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Excluir de la búsqueda de archivos huérfanos" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Exclure de la recherche de fichiers orphelins" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Kecualikan dari pencarian file yatim" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Escludi dalla ricerca di file orfani" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "孤立ファイル検索から除外" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "연결되지 않은 파일 검색에서 제외" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyklucz z wyszukiwania osieroconych plików" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Excluir da pesquisa de arquivos órfãos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Excluir da pesquisa de arquivos órfãos" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Исключить из поиска осиротевших файлов" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vylúčiť z vyhľadávania osirelých súborov" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izključi iz iskanja osirotelih datotek" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yetim dosya aramasından hariç tut" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Виключити з пошуку сирітських файлів" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Loại trừ khỏi tìm kiếm tệp mồ côi" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "从孤立文件搜索中排除" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "從孤立文件搜索中排除" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "從孤立文件搜尋中排除" } } } }, "Exclude system folders" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Systemordner ausschließen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Excluir carpetas del sistema" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Exclure les dossiers système" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Kecualikan folder sistem" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Escludi cartelle di sistema" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "システムフォルダーを除外" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "시스템 폴더 제외" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyklucz foldery systemowe" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Excluir pastas do sistema" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Excluir pastas do sistema" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Исключить системные папки" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vylúčiť systémové priečinky" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izključi sistemske mape" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sistem klasörlerini hariç tut" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Виключити системні папки" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Loại trừ thư mục hệ thống" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "排除系统文件夹" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "排除系統資料夾" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "排除系統資料夾" } } } }, "Exclude these files and folders" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Diese Dateien und Ordner ausschließen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Excluir estos archivos y carpetas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Exclure ces fichiers et dossiers" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Kecualikan file dan folder ini" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Escludi questi file e cartelle" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "これらのファイルとフォルダーを除外" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 파일 및 폴더 제외" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyklucz te pliki i foldery" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Excluir estes arquivos e pastas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Excluir estes ficheiros e pastas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Исключить эти файлы и папки" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vylúčiť tieto súbory a priečinky" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izključi te datoteke in mape" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu dosya ve klasörleri hariç tut" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Виключити ці файли та папки" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Loại trừ các tệp và thư mục này" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "排除这些文件和文件夹" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "排除這些文件和文件夾" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "排除這些檔案和資料夾" } } } }, "Exclude these files and folders from orphaned file search" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Diese Dateien und Ordner von der Suche nach verwaisten Dateien ausschließen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Excluir estos archivos y carpetas de la búsqueda de archivos huérfanos" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Exclure ces fichiers et dossiers de la recherche de fichiers résiduels" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Kecualikan file dan folder ini dari pencarian file yatim piatu" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Escludi questi file e cartelle dalla ricerca di file orfani" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "孤立ファイルの検索時に無視されるファイルやフォルダを追加します。パスをクリックするとリストから削除されます。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "연결되지 않은 파일 검색에서 이 파일 및 폴더 제외" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyklucz te pliki i foldery z wyszukiwania osieroconych plików" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Excluir esses arquivos e pastas da pesquisa de arquivos órfãos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Excluir estes ficheiros e pastas da pesquisa de ficheiros órfãos" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Исключите эти файлы и папки из поиска остаточных файлов" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vylúčiť tieto súbory a priečinky z vyhľadávania osamelých súborov" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izključi te datoteke in mape iz iskanja osirotelih datotek" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu dosya ve klasörleri başıboş dosya aramasından hariç tut" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Виключити ці файли та папки з пошуку осиротілих файлів" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Loại trừ các tệp và thư mục ra khỏi quá trình tìm kiếm tệp rác" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "在孤立文件搜索中排除这些文件和文件夹" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "從孤立檔案搜尋中排除這些檔案與資料夾" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "從孤立檔案搜尋中排除這些檔案與資料夾" } } } }, "Excluded Apps" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ausgeschlossene Apps" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Aplicaciones excluidas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Applications exclues" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Aplikasi yang Dikecualikan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "App Escluse" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "除外されたアプリ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "제외된 앱" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wykluczone aplikacje" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Aplicativos Excluídos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Aplicativos Excluídos" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Исключенные приложения" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vylúčené aplikácie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izključene aplikacije" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hariç Tutulan Uygulamalar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Виключені програми" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ứng dụng bị loại trừ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "排除的应用程序" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "排除的應用程式" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "排除應用程式" } } } }, "Excluded Paths" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ausgeschlossene Pfade" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Rutas Excluidas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Chemins d’accès exclus" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Jalur yang Dikecualikan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Percorsi esclusi" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "除外パス" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "제외된 경로" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wykluczone scieżki" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Caminhos Excluídos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Caminhos Excluídos" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Исключенные пути" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vylúčené cesty" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izključene poti" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hariç Tutulan Yollar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Виключені шляхи" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Các Đường Dẫn Bị Loại Trừ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "排除路径" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "排除的路徑" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "排除路徑" } } } }, "Export Debug Info..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Debug-Informationen exportieren..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Exportar información de depuración..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Exporter les informations de débogage..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ekspor Info Debug..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Esporta informazioni di debug..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "デバッグ情報をエクスポート..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "디버그 정보 내보내기..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Eksportuj informacje debugowania..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Exportar informações de depuração..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Exportar Informações de Depuração..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Экспортировать отладочную информацию..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Exportovať informácie o ladení..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izvozi informacije za odpravljanje napak..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hata Ayıklama Bilgilerini Dışa Aktar..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Експортувати інформацію для налагодження..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xuất thông tin gỡ lỗi..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "导出调试信息..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "導出調試信息..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "匯出調試資訊..." } } } }, "Export File Paths" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Pfad für den Export" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Exportar rutas de archivo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Exporter les chemins d’accès de fichiers" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ekspor Jalur File" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Esporta percorsi file" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ファイルパスをエクスポート" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "파일 경로 내보내기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Eksportuj ścieżki plików" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Exportar Caminhos de Arquivos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Exportar Caminhos de Arquivos" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Экспортировать пути файлов" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Exportovať cesty k súborom" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izvozi poti datotek" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Dosya Dizinlerini Dışa Aktar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Експортувати шляхи файлів" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xuất các đường dẫn tệp" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "导出文件路径" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "輸出檔案路徑" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "輸出檔案路徑" } } } }, "Export File Paths..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Exportiere Dateipfad…" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Exportar rutas de archivos..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Exporter les chemins d’accès de fichiers" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ekspor Jalur File..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Esporta Percorsi File..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ファイルパスをエクスポート..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "파일 경로 내보내기..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Eksportuj ścieżki plików..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Exportar Caminhos dos Arquivos..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Exportar Caminhos de Ficheiros..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Экспортировать пути к файлам..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Exportovať cesty k súborom..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izvozi poti datotek..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Dosya Dizinlerini Dışa Aktar…" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Експортувати шляхи файлів…" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xuất các đường dẫn tệp…" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "导出文件路径..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "輸出檔案路徑⋯" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "輸出檔案路徑⋯" } } } }, "Export Updater Debug Log..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Updater-Debug-Log exportieren..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Exportar registro de depuración del actualizador..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Exporter le journal de débogage de mise à jour..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ekspor Log Debug Updater..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Esporta il registro di debug dell'aggiornamento..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アップデータデバッグログをエクスポート..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "업데이트 디버그 로그 내보내기..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Eksportuj dziennik debugowania aktualizatora..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Exportar log de depuração do atualizador..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Exportar Log de Depuração do Atualizador..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Экспорт журнала отладки обновления..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Exportovať denník ladenia aktualizácií..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izvozi razhroščevalni dnevnik posodobitelja..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Güncelleyici Hata Ayıklama Günlüğünü Dışa Aktar..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Експорт журналу налагодження оновлень..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xuất Nhật ký Gỡ lỗi Cập nhật..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "导出更新程序调试日志..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "匯出更新器除錯日誌..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "匯出更新器偵錯日誌..." } } } }, "Extension" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Erweiterung" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Extensión" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Extension" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ekstensi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Estensione" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "拡張機能" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "확장" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Rozszerzenie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Extensão" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Extensão" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Расширение" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Rozšírenie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Razširitev" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uzantı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Розширення" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tiện ích mở rộng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "扩展" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "擴充功能" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "擴展" } } } }, "Extensions (comma-separated, e.g., jpg,png,pdf)" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Erweiterungen (durch Kommas getrennt, z.B. jpg,png,pdf)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Extensiones (separadas por comas, p. ej., jpg,png,pdf)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Extensions (séparées par des virgules, ex. : jpg,png,pdf)" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ekstensi (dipisahkan dengan koma, misalnya jpg,png,pdf)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Estensioni (separate da virgola, ad es., jpg,png,pdf)" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "拡張子(カンマで区切る、例: jpg,png,pdf)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "확장자 (쉼표로 구분, 예: jpg,png,pdf)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Rozszerzenia (oddzielone przecinkami, np. jpg,png,pdf)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Extensões (separadas por vírgula, ex.: jpg,png,pdf)" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Extensões (separadas por vírgulas, por exemplo, jpg,png,pdf)" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Расширения (через запятую, например, jpg,png,pdf)" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Rozšírenia (oddelené čiarkou, napr. jpg,png,pdf)" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Razširitve (ločene z vejico, npr. jpg,png,pdf)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uzantılar (virgülle ayrılmış, örn. jpg,png,pdf)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Розширення (через кому, напр. jpg,png,pdf)" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Phần mở rộng (ngăn cách bằng dấu phẩy, ví dụ: jpg,png,pdf)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "扩展名(以逗号分隔,例如 jpg,png,pdf)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "擴展名(以逗號分隔,例如 jpg,png,pdf)" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "擴展名(以逗號分隔,例如 jpg,png,pdf)" } } } }, "Failed to display release notes" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Versionshinweise können nicht angezeigt werden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No se pudieron mostrar las notas de la versión" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Échec de l'affichage des notes de version" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Gagal menampilkan catatan rilis" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Impossibile visualizzare le note di rilascio" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "リリースノートを表示できませんでした" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "릴리즈 노트를 표시하지 못했습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nie udało się wyświetlić informacji o wydaniu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Falha ao exibir as notas de lançamento" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Falha ao exibir as notas de lançamento" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Не удалось отобразить примечания к выпуску" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nepodarilo sa zobraziť poznámky k vydaniu" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prikaz opomb o izdaji ni uspel" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sürüm notlarını görüntüleme başarısız" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Не вдалося відобразити примітки до випуску" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Lỗi khi hiển thị ghi chú phát hành" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "无法显示更新日志" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "無法顯示版本備註" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "無法顯示版本備註" } } } }, "Fewer files" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Weniger Dateien" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Menos archivos" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Moins de fichiers" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lebih sedikit file" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Meno file" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ファイルを減らす" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "적은 파일" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Mniej plików" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Menos arquivos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Menos ficheiros" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Меньше файлов" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Menej súborov" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Manj datotek" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Daha az dosya" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Менше файлів" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ít tệp hơn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "更少文件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "較少檔案" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "更少檔案" } } } }, "File Search" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dateisuche" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Búsqueda de archivos" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Recherche de fichiers" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pencarian Berkas" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ricerca file" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ファイル検索" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "파일 검색" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyszukiwanie plików" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Busca de Arquivos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Pesquisa de Ficheiros" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Поиск файла" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vyhľadávanie súborov" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Iskanje datotek" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Dosya Arama" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Пошук файлу" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tìm kiếm tập tin" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "文件搜索" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "文件搜尋" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "文件搜尋" } } } }, "File size display options" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Optionen für die Anzeige der Dateigröße" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Opciones de visualización del tamaño del archivo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Options d'affichage de la taille du fichier" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Opsi tampilan ukuran file" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Opzioni di visualizzazione della dimensione del file" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ファイルサイズ表示オプション" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "파일 크기 표시 옵션" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Opcje wyświetlania rozmiaru pliku" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Opções de exibição do tamanho do arquivo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Opções de exibição do tamanho do ficheiro" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Параметры отображения размера файла" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Možnosti zobrazenia veľkosti súborov" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Možnosti prikaza velikosti datoteke" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Dosya boyutu görüntüleme seçenekleri" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Параметри відображення розміру файлу" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tùy chọn hiển thị kích thước tệp" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "文件大小显示选项" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "檔案大小顯示方式選項" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "檔案大小顯示方式選項" } } } }, "File System" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dateisystem" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Sistema de archivos" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Fichier Système" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Sistem Berkas" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "File System" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ファイルシステム" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "파일 시스템" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "System plików" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Sistema de Arquivos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Sistema de arquivos" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Файловая система" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Súborový systém" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Datotečni sistem" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Dosya Sistemi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Файлова система" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hệ thống Tệp" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "文件系统" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "檔案系統" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "檔案系統" } } } }, "Files no longer in trash" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dateien sind nicht mehr im Papierkorb" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Archivos ya no en la papelera" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Fichiers ne sont plus dans la corbeille" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "File tidak lagi di tempat sampah" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "File non più nel cestino" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ファイルはもうゴミ箱にありません" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "파일이 휴지통에 더 이상 없습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pliki nie są już w koszu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Arquivos não estão mais na lixeira" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ficheiros já não estão no lixo" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Файлы больше не в корзине" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Súbory už nie sú v koši" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Datoteke niso več v košu" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Dosyalar artık çöp kutusunda değil" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Файли більше не в кошику" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Các tệp không còn trong thùng rác" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "文件不再在垃圾桶中" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "檔案不再在垃圾桶中" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "檔案不再在垃圾桶中" } } } }, "Filter" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Filter" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Filtrar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Filtrer" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Saring" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Filtro" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "フィルター" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "필터" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Filtr" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Filtro" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Filtro" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Фильтр" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Filter" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Filter" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Filtre" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Фільтр" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Bộ lọc" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "筛选" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "篩選" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "篩選" } } } }, "Filter files..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dateien filtern..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Filtrar archivos..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Filtrer les fichiers..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Saring file..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Filtra file..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ファイルをフィルター..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "파일 필터링..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Filtruj pliki..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Filtrar arquivos..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Filtrar ficheiros..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Фильтровать файлы..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Filtrovať súbory..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Filtriraj datoteke..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Dosyaları filtrele..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Фільтрувати файли..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Lọc tệp..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "筛选文件..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "篩選檔案..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "篩選檔案..." } } } }, "Filter languages..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Sprachen filtern..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Filtrar idiomas..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Filtrer les langues..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Saring bahasa..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Filtra lingue..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "言語をフィルター..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "언어 필터링..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Filtruj języki..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Filtrar idiomas..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Filtrar idiomas..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Фильтровать языки..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Filtrovať jazyky..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Filtriraj jezike..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Dilleri filtrele..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Фільтрувати мови..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Lọc ngôn ngữ..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "筛选语言..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "篩選語言..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "篩選語言..." } } } }, "Filter Type" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Filtertyp" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Tipo de filtro" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Type de filtre" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Jenis Filter" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Tipo di filtro" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "フィルタータイプ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "필터 유형" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Typ filtra" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Tipo de Filtro" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Tipo de Filtro" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Тип фильтра" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Typ filtra" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Vrsta filtra" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Filtre Türü" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Тип фільтра" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Loại bộ lọc" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "过滤器类型" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "篩選類型" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "篩選類型" } } } }, "Finder Extension" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Finder-Erweiterung" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Extensión de Finder" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Extension du Finder" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ekstensi Finder" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Estensione Finder" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Finder 拡張機能" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Finder 확장" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Rozszerzenie Finder" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Extensão do Finder" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Extensão do Finder" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Расширение для Finder" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Finder rozšírenie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Razširitev Finderja" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Finder Uzantısı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Розширення Finder" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tiện ích mở rộng cho Finder" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Finder扩展" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Finder 延伸功能" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "Finder 延伸功能" } } } }, "Finding available languages..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Verfügbare Sprachen werden gesucht..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Buscando idiomas disponibles..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Recherche des langues disponibles..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Menemukan bahasa yang tersedia..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ricerca delle lingue disponibili..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "利用可能な言語を検索しています..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "사용 가능한 언어 찾는 중..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyszukiwanie dostępnych języków..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Encontrando idiomas disponíveis..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A encontrar idiomas disponíveis..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Поиск доступных языков..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Hľadanie dostupných jazykov..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Iskanje razpoložljivih jezikov..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Mevcut diller bulunuyor..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Пошук доступних мов..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang tìm ngôn ngữ có sẵn..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "查找可用语言..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "尋找可用語言..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "尋找可用語言..." } } } }, "Finding orphaned files, please wait..." : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Suche nach verwaisten Dateien, bitte warten..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Buscando archivos huérfanos, por favor espere..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Recherche de fichiers résiduels, veuillez patienter..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Sedang mencari file yatim piatu, harap tunggu..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ricerca di file orfani in corso, attendere..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "孤立ファイルを検索中、お待ちください..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "연결되지 않은 파일을 찾는 중입니다. 잠시 기다려 주세요..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyszukiwanie osieroconych plików, proszę czekać..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Encontrando arquivos órfãos, por favor, aguarde..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A encontrar ficheiros órfãos, por favor aguarde..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Поиск остаточных файлов, пожалуйста, подождите..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Hľadanie osamelých súborov, čakaj prosím..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Iskanje osirotelih datotek, prosimo počakajte..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Başıboş dosyalar bulunuyor, lütfen bekleyin..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Шукаю осиротілі файли, очікуйте, будь ласка" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang tìm kiếm tệp rác, vui lòng đợi…" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "正在查找孤立文件,请稍候..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "正在尋找孤立檔案,請稍候⋯" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "正在尋找孤立檔案,請稍候⋯" } } } }, "Folders" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ordner" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Carpetas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Dossiers" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Folder" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Cartelle" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "フォルダ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "폴더" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Foldery" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Pastas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Pastas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Папки" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Priečinky" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Mape" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Dosyalar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Папки" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thư mục" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "文件夹" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "資料夾" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "資料夾" } } } }, "Force Refresh" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Aktualisierung erzwingen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Forzar actualización" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Forcer l'actualisation" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Paksakan Segarkan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Forza aggiornamento" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "強制更新" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "강제 새로고침" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wymuś odświeżenie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar Forçadamente" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Forçar Atualização" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Принудительное обновление" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vynútiť obnovenie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prisili osvežitev" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Zorla Yenile" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Примусове Оновлення" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Làm mới ngay lập tức" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "强制刷新" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "強制重新整理" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "強制重新整理" } } } }, "Forget" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Vergessen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Olvidar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Oublier" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lupa" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Dimentica" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "削除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "잊기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zapomnij" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Esquecer" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Esquecer" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Забыть" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zabudnúť" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pozabi" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Unut" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Забути" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Quên" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "忘记" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "忘記" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "忘記" } } } }, "Formulae" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Formeln" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Fórmulas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Formules" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Rumus" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Formule" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "数式" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "조제식" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Formuły" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Fórmulas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Fórmulas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Формулы" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vzorce" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Formule" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Formüller" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Формули" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Công thức" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "公式" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "公式" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "公式" } } } }, "Formulae (%lld)" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Formeln (%lld)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Fórmulas (%lld)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Formules (%lld)" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Formulae (%lld)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Formule (%lld)" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "数式 (%lld)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "조제식 (%lld)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Formuły (%lld)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Fórmulas (%lld)" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Fórmulas (%lld)" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Формулы (%lld)" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vzorce (%lld)" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Formule (%lld)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Formüller (%lld)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Формули (%lld)" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Công thức (%lld)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "公式 (%lld)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "公式 (%lld)" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "公式 (%lld)" } } } }, "Found %lld orphaned workspace%@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%lld verwaiste Arbeitsumgebung%@ gefunden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Se encontró %lld espacio de trabajo huérfano%@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "%lld espace de travail orphelin trouvé%@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ditemukan %lld ruang kerja yatim%@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Trovato %lld spazio di lavoro orfano%@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%lld 個の孤立したワークスペース%@が見つかりました" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%lld개의 연결되지 않은 작업 공간%@을 찾았습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Znaleziono %lld osierocone obszary robocze%@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Encontrado %lld espaço de trabalho órfão%@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Encontrado %lld espaço de trabalho órfão%@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Найдено %lld остаточных рабочих пространств%@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Našiel sa %lld osirelý pracovný priestor%@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Najdenih %lld osirotelih delovnih prostorov%@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%lld yetim çalışma alanı bulundu%@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Знайдено %lld осиротілий робочий простір%@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đã tìm thấy %lld không gian làm việc mồ côi%@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "找到%lld个孤立工作区%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "找到 %lld 個孤立的工作區%@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "找到 %lld 個孤立的工作區%@" } } } }, "Found in %@ App Store. Open in App Store to update." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Gefunden im %@ App Store. Im App Store öffnen, um zu aktualisieren." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Encontrado en %@ App Store. Ábrelo en App Store para actualizar." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Trouvé dans %@ App Store. Ouvrez dans l'App Store pour mettre à jour." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ditemukan di %@ App Store. Buka di App Store untuk memperbarui." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Trovato in %@ App Store. Apri in App Store per aggiornare." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%@ App Storeで見つかりました。更新するにはApp Storeで開いてください。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@ App Store에서 찾았습니다. 업데이트하려면 App Store에서 여십시오." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Znaleziono w %@ App Store. Otwórz w App Store, aby zaktualizować." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Encontrado na %@ App Store. Abra na App Store para atualizar." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Encontrado na %@ App Store. Abra na App Store para atualizar." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Найдено в %@ App Store. Откройте в App Store для обновления." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nájdené v %@ App Store. Otvorte v App Store na aktualizáciu." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Najdeno v %@ App Store. Odprite v App Store za posodobitev." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%@ App Store'da bulundu. Güncellemek için App Store'da açın." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Знайдено в %@ App Store. Відкрийте в App Store, щоб оновити." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đã tìm thấy trong %@ App Store. Mở trong App Store để cập nhật." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "在 %@ App Store 中找到。在 App Store 中打开以更新。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "在%@ App Store中找到。打開App Store以更新。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "在%@ App Store中找到。打開App Store以更新。" } } } }, "From source" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Aus der Quelle" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Desde la fuente" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "De la source" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Dari sumber" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Dalla fonte" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ソースから" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "소스에서" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Z źródła" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Da fonte" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Da fonte" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Из источника" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Z zdroja" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Iz vira" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kaynaktan" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "З джерела" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Từ nguồn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "来自来源" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "來自來源" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "來自來源" } } } }, "Full app size" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Volle App-Größe" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Tamaño total de la aplicación" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Taille totale de l’application" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ukuran aplikasi penuh" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Dimensione completa dell'app" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アプリ全体のサイズ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "전체 앱 크기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Całkowity rozmiar aplikacji" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Tamanho total do app" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Tamanho total da aplicação" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Полный размер приложения" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Celková veľkosť aplikácie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Polna velikost aplikacije" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Tam uygulama boyutu" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Повний розмір програми" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Kích thước ứng dụng đầy đủ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "完整应用程序大小" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "完整應用程式大小" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "完整應用程式大小" } } } }, "Full Disk Access" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Festplattenvollzugriff" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Acceso Total al Disco" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Accès complet au disque" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Akses Disk Penuh" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Accesso Completo al Disco" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "完全ディスクアクセス" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "전체 디스크 접근" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pełny dostęp do dysku" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Acesso Total ao Disco" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Acesso Total ao Disco" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Полный доступ к диску" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Plný prístup k disku" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Poln dostop do diska" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Tam Disk Erişimi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Повний Доступ до Диску" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Truy cập \"Full Disk Access\"" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "完全磁盘访问权限" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "完整磁碟取用" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "完整磁碟取用" } } } }, "Functionality" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Funktionsweise" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Funcionalidad" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Fonctionnalité" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Fungsionalitas" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Funzionalità" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "機能" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "기능" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Funkcjonalność" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Funcionalidade" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Funcionalidade" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Функциональность" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Funkcie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Funkcionalnost" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İşlev" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Функціональність" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chức năng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "功能" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "功能" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "功能" } } } }, "Gathering app details" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Sammeln von Programmdetails" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Reuniendo detalles de la aplicación" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Collecte des détails de l'application" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Mengumpulkan detail aplikasi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Raccolta dei dettagli dell'app" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アプリの詳細を収集中" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "앱 세부 정보 수집 중" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zbieranie szczegółowych informacji o aplikacji" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Coletando detalhes do aplicativo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Recolhendo detalhes do aplicativo" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Получение данных о приложении" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zhromažďovanie údajov o aplikácii" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Zbiranje podrobnosti aplikacije" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uygulama detayları toplanıyor" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Збираю дані про програму" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang tải thông tin ứng dụng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "正在收集应用详情" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "正在收集 App 詳細資料" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "正在收集 App 詳細資料" } } } }, "GB" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "GB" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "GB" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "GB" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "GB" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "GB" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "GB" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "GB" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "GB" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "GB" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "GB" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "GB" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "GB" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "GB" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "GB" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "GB" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "GB" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "GB" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "GB" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "GB" } } } }, "General" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Allgemeine" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "General" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Général" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Umum" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Generale" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "一般" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "일반" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ogólne" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Geral" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Geral" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Основные" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Všeobecné" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Splošno" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Genel" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Загальні" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Cài đặt chung" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "通用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "一般" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "一般" } } } }, "GitHub Sponsors" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "GitHub Sponsoren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Patrocinadores de GitHub" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "GitHub Sponsors" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Sponsor GitHub" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Sponsor di GitHub" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "GitHubスポンサー" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "GitHub 후원자" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Sponsorzy GitHub" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Patrocinadores do GitHub" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Patrocinadores do GitHub" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Спонсоры GitHub" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Github Sponzori" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Sponzorji GitHub" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "GitHub Sponsorları" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Спонсори GitHub" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Những nhà tài trợ trên Github" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "GitHub 赞助者" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "GitHub 贊助者" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "GitHub 贊助者" } } } }, "GitHub: v%@" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "GitHub: v%@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "GitHub: v%@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "GitHub : v%@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "GitHub: v%@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "GitHub: v%@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "GitHub: v%@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "GitHub: v%@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "GitHub: v%@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "GitHub: v%@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "GitHub: v%@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "GitHub: v%@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "GitHub: v%@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "GitHub: v%@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "GitHub: v%@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "GitHub: версія %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Github v%@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "GitHub:v%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "GitHub:v%@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "GitHub:v%@" } } } }, "Glass Effect" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Glaseffekt" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Efecto de Vidrio" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Effet de verre" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Efek Kaca" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Effetto vetro" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ガラス効果" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "유리 효과" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Efekt szkła" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Efeito de Vidro" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Efeito de Vidro" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Эффект стекла" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Sklenený efekt" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Učinek stekla" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Cam Efekti" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ефект скла" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hiệu ứng kính" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "玻璃效果" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "玻璃效果" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "玻璃效果" } } } }, "Greeting disabled on main page" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Begrüßung auf der Hauptseite deaktiviert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "El saludo está desactivado en la página principal" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Message d'accueil désactivé sur la page principale" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Salam dinonaktifkan di halaman utama" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Saluto disabilitato nella pagina principale" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "メインページでの挨拶が無効化されています" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "메인 페이지에서 인사말 비활성화됨" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyłączone powitanie na stronie głównej" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Cumprimento desativado na página principal" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Saudação desativada na página principal" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Приветствие отключено на главной странице" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pozdrav na hlavnej stránke vypnutý" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pozdrav na glavni strani onemogočen" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ana sayfada karşılama devre dışı bırakıldı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Вітання вимкнено на головній сторінці" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chào mừng đã bị tắt trên trang chính" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "主页面问候已禁用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "主頁已禁用問候語" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "主頁問候語已停用" } } } }, "Greeting enabled on main page" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Begrüßung auf der Hauptseite aktiviert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Saludo habilitado en la página principal" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Message d'accueil désactivé sur la page principale" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Salam diaktifkan di halaman utama" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Saluto abilitato nella pagina principale" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "メインページでの挨拶が有効" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "메인 페이지에서 인사말 활성화" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Włączone powitanie na stronie głównej" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Saudação ativada na página principal" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Saudação ativada na página principal" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Приветствие включено на главной странице" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pozdrav na hlavnej stránke je povolený" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pozdrav omogočen na glavni strani" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ana sayfada karşılama etkinleştirildi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Привітання увімкнено на головній сторінці" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đã bật câu chào trên trang chính" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "主页问候已启用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "主頁啟用問候語" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "主頁問候已啟用" } } } }, "Grid View" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Rasteransicht" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Vista de cuadrícula" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Vue Grille" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tampilan Grid" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Vista griglia" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "グリッドビュー" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "그리드 보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Widok siatki" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Visualização em Grade" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Vista em Grade" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Представление в виде сетки" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobrazenie mriežky" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pogled mreže" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Izgara Görünümü" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Подання сітки" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chế độ xem lưới" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "网格视图" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "網格檢視" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "網格檢視" } } } }, "Helper" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Helper" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Asistente" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Assistant" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pembantu" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Assistente" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ヘルパー" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "도우미" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Narzędzie pomocnicze" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Assistente" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Assistente" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Помощник" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pomocník" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pomočnik" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yardımcı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Помічник" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Phụ trợ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "辅助服务" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "輔助應用程式" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "輔助應用程式" } } } }, "Helper Playground" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Helfer-Spielplatz" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Área de Pruebas del Asistente" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Terrain de Jeu de l'Assistant" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Taman Bermain Pembantu" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Area di gioco dell'assistente" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ヘルパーの遊び場" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "도우미 놀이터" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Plac Zabaw Pomocnika" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Playground do Assistente" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Área de Testes do Assistente" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Площадка помощника" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Ihrisko pomocníka" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Igralnica Pomočnika" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yardımcı Oyun Alanı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Майданчик помічника" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Sân chơi Trợ lý" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "助手游乐场" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "助手遊樂場" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "助手遊樂場" } } } }, "Helper tool needs to be enabled" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Hilfswerkzeug muss aktiviert werden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "La herramienta de ayuda debe estar habilitada" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "L'outil d'assistance doit être activé" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Alat bantu perlu diaktifkan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Lo strumento di supporto deve essere abilitato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ヘルパーツールを有効にする必要があります" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "도우미 도구를 활성화해야 합니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Narzędzie pomocnicze musi być włączone" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "A ferramenta auxiliar precisa ser ativada" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A ferramenta auxiliar precisa ser ativada" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Необходимо включить вспомогательный инструмент" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pomocný nástroj musí byť povolený" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pomožno orodje mora biti omogočeno" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yardımcı aracın etkinleştirilmesi gerekiyor" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Потрібно увімкнути допоміжний інструмент" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Công cụ trợ giúp cần được kích hoạt" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "需要启用辅助工具" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "需要啟用輔助工具" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "需要啟用輔助工具" } } } }, "Hidden" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Versteckt" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Oculto" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Caché" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tersembunyi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nascosto" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "非表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "숨김" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ukryty" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Oculto" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Oculto" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Скрыто" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Skryté" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Skrito" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Gizli" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Приховано" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ẩn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "隐藏" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "隱藏" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "隱藏" } } } }, "Hide auto-updating apps from Homebrew" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Automatisch aktualisierte Apps von Homebrew ausblenden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ocultar aplicaciones de actualización automática de Homebrew" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Masquer les applications à mise à jour automatique de Homebrew" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Sembunyikan aplikasi yang memperbarui otomatis dari Homebrew" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nascondi le app che si aggiornano automaticamente da Homebrew" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Homebrewの自動更新アプリを非表示にする" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew에서 자동 업데이트 앱 숨기기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ukryj aplikacje z automatyczną aktualizacją z Homebrew" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ocultar aplicativos de atualização automática do Homebrew" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ocultar apps de atualização automática do Homebrew" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Скрыть автоматически обновляемые приложения из Homebrew" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Skryť automaticky aktualizované aplikácie z Homebrew" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Skrij samodejno posodabljanje aplikacij iz Homebrewa" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew'den otomatik güncellenen uygulamaları gizle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Приховати програми з автоматичним оновленням з Homebrew" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ẩn ứng dụng tự động cập nhật từ Homebrew" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "隐藏来自 Homebrew 的自动更新应用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "隱藏來自 Homebrew 的自動更新應用程式" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "隱藏來自 Homebrew 的自動更新應用程式" } } } }, "Hide details" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Details ausblenden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ocultar detalles" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Cacher les détails" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Sembunyikan detail" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nascondi dettagli" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "詳細を非表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "세부 정보 숨기기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ukryj szczególy" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ocultar detalhes" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ocultar detalhes" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Скрыть подробности" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Skryť podrobnosti" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Skrij podrobnosti" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ayrıntıları gizle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Приховати деталі" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ẩn chi tiết" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "隐藏详细信息" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "隱藏詳細資料" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "隱藏詳細資料" } } } }, "Hide multi-select" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Mehrfachauswahl ausblenden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ocultar selección múltiple" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Cacher la sélection multiple" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Sembunyikan multi-pilih" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nascondi selezione multipla" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "複数選択を非表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "다중 선택 숨기기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ukryj wielokrotny wybór" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ocultar seleção múltipla" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ocultar seleção múltipla" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Скрыть множественный выбор" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Skryť viacnásobný výber" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Skrij večkratni izbor" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Çoklu seçim gizle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Приховати багатоселектор" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ẩn chọn nhiều" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "隐藏多选" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "隱藏多選" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "隱藏多選" } } } }, "Hide Packages" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Pakete ausblenden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ocultar paquetes" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Masquer les paquets" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Sembunyikan Paket" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nascondi pacchetti" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "パッケージを隠す" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "패키지 숨기기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ukryj pakiety" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ocultar Pacotes" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ocultar Pacotes" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Скрыть пакеты" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Skryť balíky" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Skrij pakete" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Paketleri Gizle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Приховати пакунки" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ẩn gói" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "隐藏包" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "隱藏套件" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "隱藏套件" } } } }, "Hide page" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Seite ausblenden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ocultar página" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Masquer la page" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Sembunyikan halaman" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nascondi pagina" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ページを非表示にする" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "페이지 숨기기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ukryj stronę" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ocultar página" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ocultar página" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Скрыть страницу" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Skryť stránku" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Skrij stran" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sayfayı gizle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Приховати сторінку" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ẩn trang" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "隐藏页面" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "隱藏頁面" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "隱藏頁面" } } } }, "Homebrew" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew" } } } }, "Homebrew cleanup after uninstall" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Aufräumen von Homebrew nach der Deinstallation" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Limpieza de Homebrew después de desinstalar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Nettoyage avec l’utilitaire Homebrew après une désinstallation" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pembersihan Homebrew setelah pencopotan pemasangan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Pulizia Homebrew dopo la disinstallazione" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アンインストール後にHomebrewをクリーンアップ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "제거 후 Homebrew 정리" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Uruchom czyszczenie homebrew po odinstalowaniu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Limpeza do Homebrew após desinstalação" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Limpeza do Homebrew após desinstalação" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Очистка Homebrew после удаления" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vyčistiť Homebrew po odinštalácii" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Čiščenje Homebrew po odstranitvi" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kaldırdıktan sonra Homebrew temizliği" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Очистка Homebrew після видалення" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chạy \"homebrew cleanup\" sau khi gỡ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "卸载后清理 Homebrew" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "解除安裝後執行 Homebrew 清理" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "解除安裝後執行 Homebrew 清理" } } } }, "Homebrew collects anonymous analytics to help improve the project" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew sammelt anonyme Analysen, um das Projekt zu verbessern" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew recopila análisis anónimos para ayudar a mejorar el proyecto" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew collecte des analyses anonymes pour aider à améliorer le projet" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew mengumpulkan analitik anonim untuk membantu meningkatkan proyek" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew raccoglie analisi anonime per aiutare a migliorare il progetto" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Homebrewはプロジェクトを改善するために匿名の分析を収集します" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew는 프로젝트 개선을 위해 익명의 분석 데이터를 수집합니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew zbiera anonimowe dane analityczne, aby pomóc w ulepszaniu projektu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew coleta análises anônimas para ajudar a melhorar o projeto" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "O Homebrew recolhe análises anónimas para ajudar a melhorar o projeto" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew собирает анонимную аналитику, чтобы помочь улучшить проект" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew zhromažďuje anonymné analytické údaje na zlepšenie projektu" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew zbira anonimne analize za pomoč pri izboljšanju projekta" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew, projeyi geliştirmeye yardımcı olmak için anonim analizler toplar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew збирає анонімну аналітику, щоб покращити проект" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew thu thập phân tích ẩn danh để giúp cải thiện dự án" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 收集匿名分析以帮助改进项目" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 收集匿名分析以幫助改進項目" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 收集匿名分析以幫助改善項目" } } } }, "Homebrew Doctor" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew Doktor" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Doctor Homebrew" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Docteur Homebrew" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Dokter Homebrew" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew Doctor" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ホームブルー医師" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 진단기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Doktor Homebrew" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Doutor Homebrew" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew Doctor" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Домашний врач" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Domáci lekár" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew zdravnik" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew Doktor" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Домашній лікар" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Bác sĩ Homebrew" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 诊断" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 診斷" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 診斷" } } } }, "Homebrew is not installed on your system. This feature requires Homebrew to manage packages." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew ist nicht auf Ihrem System installiert. Diese Funktion erfordert Homebrew zur Verwaltung von Paketen." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew no está instalado en su sistema. Esta función requiere Homebrew para gestionar paquetes." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew n'est pas installé sur votre système. Cette fonctionnalité nécessite Homebrew pour gérer les paquets." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew tidak terpasang di sistem Anda. Fitur ini memerlukan Homebrew untuk mengelola paket." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew non è installato sul tuo sistema. Questa funzione richiede Homebrew per gestire i pacchetti." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew はシステムにインストールされていません。この機能にはパッケージを管理するために Homebrew が必要です。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "시스템에 Homebrew가 설치되어 있지 않습니다. 이 기능은 패키지 관리를 위해 Homebrew가 필요합니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew nie jest zainstalowany w twoim systemie. Ta funkcja wymaga Homebrew do zarządzania pakietami." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew não está instalado no seu sistema. Este recurso requer Homebrew para gerenciar pacotes." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "O Homebrew não está instalado no seu sistema. Este recurso requer o Homebrew para gerenciar pacotes." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew не установлен на вашей системе. Эта функция требует Homebrew для управления пакетами." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew nie je nainštalovaný vo vašom systéme. Táto funkcia vyžaduje Homebrew na správu balíkov." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew ni nameščen v vašem sistemu. Ta funkcija zahteva Homebrew za upravljanje paketov." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew sisteminizde yüklü değil. Bu özellik, paketleri yönetmek için Homebrew gerektirir." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew не встановлено на вашій системі. Ця функція вимагає Homebrew для управління пакетами." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew chưa được cài đặt trên hệ thống của bạn. Tính năng này yêu cầu Homebrew để quản lý các gói." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 未安装在您的系统上。此功能需要 Homebrew 来管理软件包。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 未安裝在您的系統上。此功能需要 Homebrew 來管理套件。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 未安裝在您的系統上。此功能需要 Homebrew 來管理套件。" } } } }, "Homebrew is up to date" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew ist auf dem neuesten Stand" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew está actualizado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew est à jour" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew sudah terbaru" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew è aggiornato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew は最新です" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew가 최신 상태입니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew jest aktualny" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew está atualizado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew está atualizado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew обновлён" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew je aktuálny" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew je posodobljen" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew güncel" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew оновлено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew đã được cập nhật" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 是最新的" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 是最新的" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 是最新的" } } } }, "Homebrew Manager" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew-Manager" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Administrador de Homebrew" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Gestionnaire Homebrew" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pengelola Homebrew" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Gestore Homebrew" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew マネージャー" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 관리자" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Menedżer Homebrew" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Gerenciador de Homebrew" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Gerenciador do Homebrew" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Менеджер Homebrew" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Správca Homebrew" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Upravitelj Homebrew" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew Yöneticisi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Менеджер Homebrew" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Trình quản lý Homebrew" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 管理器" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 管理器" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 管理器" } } } }, "Homebrew Not Installed" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew ist nicht installiert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew no está instalado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew n'est pas installé" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew belum terpasang" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew non installato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew がインストールされていません" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew가 설치되지 않음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew nie jest zainstalowany" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew não está instalado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew Não Instalado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew не установлен" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew nie je nainštalovaný" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew ni nameščen" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew yüklü değil" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew не встановлено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew chưa được cài đặt" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 未安装" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 未安裝" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 未安裝" } } } }, "Homebrew update available" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew-Update verfügbar" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualización de Homebrew disponible" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mise à jour Homebrew disponible" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pembaruan Homebrew tersedia" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiornamento Homebrew disponibile" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew の更新が利用可能です" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 업데이트 가능" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dostępna aktualizacja Homebrew" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualização do Homebrew disponível" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Atualização do Homebrew disponível" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Доступно обновление Homebrew" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizácia Homebrew je k dispozícii" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Na voljo je posodobitev Homebrew" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew güncellemesi mevcut" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Доступне оновлення Homebrew" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Có bản cập nhật Homebrew" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 更新可用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 更新可用" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 更新可用" } } } }, "Homebrew Version" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew-Version" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Versión de Homebrew" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Version Homebrew" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Versi Homebrew" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Versione Homebrew" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Homebrewバージョン" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 버전" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wersja Homebrew" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Versão Homebrew" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Versão do Homebrew" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Версия Homebrew" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Verzia Homebrew" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Različica Homebrew" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew Sürümü" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Версія Homebrew" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Phiên bản Homebrew" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 版本" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 版本" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 版本" } } } }, "Homepage" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Startseite" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Página de inicio" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Page d'accueil" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Beranda" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Pagina iniziale" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ホームページ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "홈페이지" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Strona główna" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Página inicial" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Página inicial" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Главная страница" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Domovská stránka" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Domača stran" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ana Sayfa" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Домашня сторінка" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Trang chủ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "首页" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "首頁" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "主頁" } } } }, "ID: %@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "ID: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "ID: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "ID: %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "ID: %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "ID: %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ID: %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "ID: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "ID: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "ID: %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "ID: %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Идентификатор: %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "ID: %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "ID: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kimlik: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "ID: %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "ID: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "ID: %@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "ID: %@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "ID: %@" } } } }, "If the correct cask isn't listed above, enter the cask token manually:" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Wenn der richtige Cask oben nicht aufgeführt ist, geben Sie das Cask-Token manuell ein:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Si el barril correcto no está listado arriba, ingrese el token del barril manualmente:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Si le fût correct n'est pas répertorié ci-dessus, saisissez le jeton du fût manuellement :" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Jika tong yang benar tidak terdaftar di atas, masukkan token tong secara manual:" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Se la botte corretta non è elencata sopra, inserisci manualmente il token della botte:" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "正しいカスクが上にリストされていない場合は、カスクトークンを手動で入力してください:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "올바른 캐스크가 위에 나열되지 않은 경우 캐스크 토큰을 수동으로 입력하십시오:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Jeśli właściwa beczka nie jest wymieniona powyżej, wprowadź ręcznie token beczki:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Se o barril correto não estiver listado acima, insira o token do barril manualmente:" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Se o barril correto não estiver listado acima, insira o token do barril manualmente:" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Если нужная бочка не указана выше, введите токен бочки вручную:" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Ak správny sud nie je uvedený vyššie, zadajte token suda manuálne:" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Če pravilna sodčka ni na seznamu zgoraj, ročno vnesite žeton sodčka:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Doğru fıçı yukarıda listelenmemişse, fıçı jetonunu manuel olarak girin:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Якщо правильна бочка не вказана вище, введіть токен бочки вручну:" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Nếu thùng chính xác không được liệt kê ở trên, hãy nhập mã thùng thủ công:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "如果正确的桶未在上面列出,请手动输入桶代码:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "如果正確的桶未在上面列出,請手動輸入桶代碼:" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "如果正確的桶未在上面列出,請手動輸入桶代碼:" } } } }, "Important" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Wichtig" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Importante" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Important" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Penting" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Importante" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "重要" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "중요" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ważne" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Importante" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Importante" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Важно" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Dôležité" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pomembno" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Önemli" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Важливо" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Quan trọng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "重要" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "重要" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "重要" } } } }, "Include hidden files" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Versteckte Dateien einbeziehen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Incluir archivos ocultos" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Inclure les fichiers cachés" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Sertakan file tersembunyi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Includi file nascosti" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "隠しファイルを含める" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "숨김 파일 포함" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Uwzględnij ukryte pliki" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Incluir arquivos ocultos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Incluir ficheiros ocultos" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Включить скрытые файлы" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zahrnúť skryté súbory" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Vključi skrite datoteke" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Gizli dosyaları dahil et" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Включити приховані файли" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Bao gồm các tệp ẩn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "包括隐藏文件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "包括隱藏檔案" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "包括隱藏檔案" } } } }, "Include subfolders" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Unterordner einbeziehen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Incluir subcarpetas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Inclure les sous-dossiers" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Sertakan subfolder" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Includi sottocartelle" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "サブフォルダーを含める" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "하위 폴더 포함" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Uwzględnij podfoldery" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Incluir subpastas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Incluir subpastas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Включить подпапки" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zahrnúť podpriečinky" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Vključi podmape" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Alt klasörleri dahil et" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Включити підпапки" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Bao gồm thư mục con" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "包含子文件夹" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "包含子資料夾" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "包括子資料夾" } } } }, "Info" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Info" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Información" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Infos" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Info" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Informazioni" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "情報" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "정보" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Informacje" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Informações" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Informação" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Информация" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Informácie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Informacije" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bilgi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Інформація" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thông tin" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "信息" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "資訊" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "資訊" } } } }, "Information" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Information" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Información" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Information" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Informasi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Informazioni" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "お知らせ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "정보" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Informacje" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Informação" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Informação" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Информация" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Informácie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Informacije" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bilgi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Інформація" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thông tin" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "信息" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "訊息" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "訊息" } } } }, "Install" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Installieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Instalar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Installer" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Instalasi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Installa" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "インストール" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "설치" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zainstalować" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Instalar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Instalar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Установить" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Inštalovať" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Namesti" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yükle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Встановити" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Cài đặt" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "安装" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "安裝" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "安裝" } } } }, "Install %@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Installieren %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Instalar %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Installer %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Instal %@ " } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Installa %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "インストール %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@ 설치" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zainstalować %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Instalar %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Instalar %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Установить %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Inštalovať %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Namesti %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yükle %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Встановити %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Cài đặt %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "安装%@ " } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "安裝%@ " } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "安裝%@ " } } } }, "Install %@?" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Installieren %@?" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "¿Instalar %@?" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Installer %@?" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Instal %@?" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Installare %@?" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "インストール %@?" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@ 설치하시겠습니까?" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zainstalować %@?" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Instalar %@?" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Instalar %@?" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Установить %@?" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nainštalovať %@?" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Namestiti %@?" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yükle %@?" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Встановити %@?" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Cài đặt %@?" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "安装 %@?" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "安裝 %@?" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "安裝 %@?" } } } }, "Install Homebrew" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew installieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Instalar Homebrew" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Installer Homebrew" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Instal Homebrew" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Installa Homebrew" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Homebrewをインストール" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 설치" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zainstaluj Homebrew" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Instalar Homebrew" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Instalar Homebrew" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Установить Homebrew" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nainštalovať Homebrew" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Namesti Homebrew" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew Yükle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Встановити Homebrew" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Cài đặt Homebrew" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "安装 Homebrew" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "安裝 Homebrew" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "安裝 Homebrew" } } } }, "Install Location:" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Installationsort:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ubicación de instalación:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Emplacement de l’installation :" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lokasi Instal:" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Posizione di installazione:" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "インストール場所:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "설치 위치:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Lokalizacja:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Local de Instalação:" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Local de Instalação:" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Место установки:" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Umiestnenie inštalácie:" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Lokacija namestitve:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yükleme Konumu:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Місце встановлення:" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Vị trí cài đặt:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "安装位置:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "安裝位置:" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "安裝位置:" } } } }, "Installation" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Installation" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Instalación" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Installation" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Instalasi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Installazione" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "インストール" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "설치" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Instalacja" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Instalação" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Instalação" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Установка" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Inštalácia" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Namestitev" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kurulum" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Встановлення" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Cài đặt" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "安装" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "安裝" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "安裝" } } } }, "Installation failed: %@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Installation fehlgeschlagen: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Instalación fallida: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Echec de l’installation: %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Instalasi gagal: %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Installazione fallita: %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "インストールに失敗しました: %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "설치 실패: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Instalacja nie powiodła się: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Falha na instalação: %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Falha na instalação: %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Ошибка установки: %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Inštalácia zlyhala: %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Namestitev ni uspela: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kurulum başarısız oldu: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Помилка встановлення: %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Cài đặt thất bại: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "安装失败: %@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "安裝失敗:%@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "安裝失敗:%@" } } } }, "Installed" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Installiert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Instalado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Installé" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Terpasang" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Installato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "インストール済み" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "설치됨" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zainstalowano" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Instalado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Instalado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Установлено" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nainštalované" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Nameščeno" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yüklendi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Встановлено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đã cài đặt" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "已安装" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "已安裝" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "已安裝" } } } }, "Installed from tap: %@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Installiert von Tap: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Instalado desde tap: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Installé depuis tap : %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Diinstal dari tap: %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Installato da tap: %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "タップからインストール: %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "탭에서 설치됨: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zainstalowano z tap: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Instalado a partir do tap: %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Instalado a partir de tap: %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Установлено из tap: %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nainštalované z tap: %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Nameščeno iz vira: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Tap'tan yüklendi: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Встановлено з tap: %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đã cài đặt từ tap: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "从 tap 安装:%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "從 tap 安裝:%@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "從 tap 安裝:%@" } } } }, "Installed:" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Installiert:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Instalado:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Installé(es):" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Terpasang:" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Installato:" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "インストール済み:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "설치됨:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zainstalowano:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Instalado:" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Instalado:" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Установлено:" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nainštalované:" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Nameščeno:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yüklendi:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Встановлено:" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đã cài đặt:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "已安装:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "安裝日期:" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "已安裝:" } } } }, "Installed: v%@" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Installiert: v%@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Instalado: v%@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Installé : v%@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Terpasang: v%@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Installato: v%@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "インストール済み: v%@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "설치됨: v%@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zainstalowano: v%@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Instalado: v%@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Instalado: v%@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Установлено: v%@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nainštalované: v%@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Nameščeno: v%@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kurulum: v%@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Інстальовано: v%@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đã cài đặt v%@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "已安装:v%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "已安裝:v%@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "已安裝:v%@" } } } }, "Installer:" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Installer:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Instalador:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Moyen d’installation" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pemasang:" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Installatore:" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "インストーラー:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "설치 프로그램:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Instalator:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Instalador:" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Instalador:" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Установщик:" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Inštalátor:" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Namestitveni program:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yükleyici:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Інсталятор:" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Trình cài đặt:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "安装程序:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "安裝程式:" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "安裝程式:" } } } }, "Installing..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Installieren..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Instalando..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Installation..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Menginstal..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Installazione in corso..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "インストール中..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "설치 중..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Instalowanie..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Instalando..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Instalando..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Установка..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Inštalácia..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Nameščanje..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yükleniyor..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Встановлення..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang cài đặt..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "正在安装..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "正在安裝..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "正在安裝..." } } } }, "Instructions" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Anleitung" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Instrucciones" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Instructions" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Instruksi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Istruzioni" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "指示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "지침" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Instrukcje" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Instruções" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Instruções" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Инструкции" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Inštrukcie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Navodila" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Talimatlar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Інструкції" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hướng dẫn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "说明" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "指示" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "指示" } } } }, "Interface" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Schnittstelle" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Interfaz" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Interface" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Antarmuka" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Interfaccia" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "インターフェース" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "인터페이스" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Interfejs" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Interface" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Interface" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Интерфейс" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Rozhranie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Vmesnik" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Arayüz" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Інтерфейс" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Giao diện" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "界面" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "介面" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "介面" } } } }, "Invalid signature, ensure proper signing on the application and helper tool." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ungültige Signatur, stellen Sie sicher, dass die Anwendung und das Hilfsprogramm ordnungsgemäß signiert sind." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Firma no válida, asegúrese de firmar correctamente en la aplicación y la herramienta auxiliar." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Signature invalide. Vérifiez que l’application porte votre signature correcte et l'outil d'aide." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tanda tangan tidak valid, pastikan penandatanganan yang benar pada aplikasi dan alat bantu." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Firma non valida, assicurati che l'applicazione e lo strumento di supporto siano firmati correttamente." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "無効な署名です。アプリケーションとヘルパーツールの適切な署名を確認してください。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "잘못된 서명입니다. 애플리케이션과 도우미 도구에 올바른 서명이 있는지 확인하십시오." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nieprawidłowy podpis, upewnij się, że aplikacja i narzędzie pomocnicze zostały poprawnie podpisane." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Assinatura inválida, certifique-se de assinar corretamente o aplicativo e a ferramenta auxiliar." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Assinatura inválida, certifique-se de que a aplicação e a ferramenta auxiliar estão devidamente assinadas." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Недействительная подпись, убедитесь, что приложение и вспомогательный инструмент правильно подписаны." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Neplatný podpis, uistite sa, že aplikácia a pomocný nástroj sú riadne podpísané." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Neveljaven podpis, zagotovite pravilno podpisovanje aplikacije in pomožnega orodja." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Geçersiz imza, uygulamada ve yardımcı araçta doğru imzalamayı sağlayın." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Недійсний підпис, переконайтесь у правильності підписання програми та допоміжного інструменту." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chữ ký không hợp lệ, đảm bảo rằng ứng dụng và công cụ hỗ trợ đã được sử dụng đúng cách." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "无效签名,确保应用程序和辅助工具正确签名。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "無效的簽名。確保應用程式與輔助應用程式工具已正確簽署。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "無效的簽名。確保應用程式與輔助應用程式工具已正確簽署。" } } } }, "iOS" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "iOS" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "iOS" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "iOS" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "iOS" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "iOS" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "iOS" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "iOS" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "iOS" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "iOS" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "iOS" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "iOS" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "iOS" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "iOS" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "iOS" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "iOS" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "iOS" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "iOS" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "iOS" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "iOS" } } } }, "iOS app" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "iOS-App" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Aplicación de iOS" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Application iOS" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "aplikasi iOS" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "App iOS" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "iOS アプリ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "iOS 앱" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Aplikacja iOS" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "app do iOS" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Aplicação iOS" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "iOS приложение" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "IOS aplikácia" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "iOS aplikacija" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "iOS uygulaması" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Програма iOS" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ứng dụng iOS" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "iOS 应用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "iOS 應用程式" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "iOS 應用程式" } } } }, "Issues detected:" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Probleme erkannt:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Problemas detectados:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Problèmes détectés :" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Masalah terdeteksi:" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Problemi rilevati:" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "問題が検出されました:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "문제 감지됨:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wykryto problemy:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Problemas detectados:" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Problemas detectados:" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Обнаружены проблемы:" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zistené problémy:" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Zaznane težave:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Tespit edilen sorunlar:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Виявлено проблеми:" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Vấn đề được phát hiện:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "检测到问题:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "檢測到問題:" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "檢測到問題:" } } } }, "item" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Element" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "artículo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "élément" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "item" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "elemento" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アイテム" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "항목" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "rzecz" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "item" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "item" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "элемент" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "položka" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "element" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "öğe" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "елемент" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "mục" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "项目" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "項目" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "項目" } } } }, "items" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Elemente" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "artículos" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "éléments" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "item" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "elementi" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アイテム" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "항목" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "rzeczy" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "itens" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "itens" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "элементы" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "položiek" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "elementi" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "öğeden" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "елементи" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Mục" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "项" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "項目" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "項目" } } } }, "KB" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "KB" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "KB" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "KB" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "KB" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "KB" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "KB" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "KB" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "KB" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "KB" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "KB" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "KB" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "KB" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "KB" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "KB" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "KB" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "KB" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "KB" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "KB" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "KB" } } } }, "Keep Alive: Always" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Immer aktiv halten" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Mantener activo: Siempre" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Garder actif : Toujours" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tetap Aktif: Selalu" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Mantieni attivo: Sempre" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "常にアクティブに保つ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "항상 활성상태 유지" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Utrzymuj aktywność: Zawsze" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Manter Ativo: Sempre" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Manter Ativo: Sempre" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Оставаться активным: Всегда" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Udržiavať aktívne: Vždy" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ohrani aktivno: Vedno" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Her Zaman Aktif Tut" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Залишатися активним: Завжди" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Luôn hoạt động: Luôn luôn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "保持活跃:始终" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "保持活躍:始終" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "保持活躍:始終" } } } }, "Kind" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Art" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Tipo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Type" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Jenis" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Tipo" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "種類" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "종류" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Rodzaj" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Tipo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Tipo" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Тип" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Druh" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Vrsta" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Tür" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Тип" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Loại" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "种类" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "種類" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "種類" } } } }, "Last modified: %@" : { }, "Last Used Date" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Zuletzt benutzt am" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Fecha de último uso" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Date de dernière utilisation" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tanggal Terakhir Digunakan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Data Ultimo Utilizzo" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "最終使用日" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "마지막 사용 날짜" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Data ostatniego użycia" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Data do último uso" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Data da Última Utilização" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Дата последнего использования" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Dátum posledného použitia" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Zadnji datum uporabe" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kullanıldığı Son Tarih" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Використано востаннє" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ngày sử dụng gần nhất" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "上次使用日期" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "上次使用日期" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "上次使用日期" } } } }, "Launch Pearcleaner at login" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Starte Pearcleaner beim Start" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Iniciar Pearcleaner al iniciar sesión" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Démarrer Pearcleaner au démarrage " } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Luncurkan Pearcleaner saat masuk" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Avvia Pearcleaner all'accesso" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ログイン時にPearcleanerを起動" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "로그인 시 Pearcleaner 실행" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Uruchom Pearcleaner podczas logowania" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Iniciar Pearcleaner no login" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Iniciar Pearcleaner ao iniciar sessão" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Запускать Pearcleaner при входе в систему" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Spustiť Pearcleaner pri prihlásení" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Zaženi Pearcleaner ob prijavi" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Oturum açıldığında Pearcleaner’ı başlat" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Запускати Pearcleaner після логіну" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Mở Pearcleaner khi đăng nhập" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "在登录时启动 Pearcleaner" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "登入時啟動 Pearcleaner" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "登入時啟動 Pearcleaner" } } } }, "Launch Services Manager" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Startdienste-Manager" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Administrador de Servicios de Lanzamiento" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Gestionnaire des services de lancement" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Luncurkan Manajer Layanan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Avvia Gestore Servizi" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Launch Services マネージャー" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "서비스 관리자 실행" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zarządzanie usługami startowymi" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Gerenciador de Serviços de Inicialização" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Iniciar Gestor de Serviços" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Менеджер служб запуска" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Správca služieb spúšťania" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Zaženi upravitelja storitev" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Başlatma Hizmetleri Yöneticisi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Менеджер служб запуску" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Trình Quản Lý Dịch Vụ Khởi Chạy" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "启动服务管理器" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "啟動服務管理員" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "啟動服務管理器" } } } }, "LaunchAgent is active" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "LaunchAgent ist aktiv" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "LaunchAgent está activo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "LaunchAgent est actif" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "LaunchAgent aktif" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "LaunchAgent è attivo" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "LaunchAgentがアクティブです" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "LaunchAgent가 활성화되었습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "LaunchAgent jest aktywny" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "LaunchAgent está ativo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "LaunchAgent está ativo" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "LaunchAgent активен" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "LaunchAgent je aktívny" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "LaunchAgent je aktiven" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "LaunchAgent etkin" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "LaunchAgent активний" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "LaunchAgent đang hoạt động" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "LaunchAgent 已激活" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "LaunchAgent 已啟動" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "LaunchAgent 已啟動" } } } }, "Layout" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Layout" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Diseño" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Disposition" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tata Letak" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Layout" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "レイアウト" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "레이아웃" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Układ" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Layout" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Layout" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Макет" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Rozloženie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Postavitev" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Düzen" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Макет" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Bố cục" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "布局" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "佈局" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "版面配置" } } } }, "Learn More" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Mehr erfahren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Más información" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "En savoir plus" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pelajari lebih lanjut" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ulteriori informazioni" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "詳細を確認する" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "자세히 알아보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dowiedz się więcej" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Saiba mais" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Saiba Mais" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Узнать больше" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zistiť viac" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izvedi več" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Daha fazla bilgi edinin" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Дізнатися більше" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tìm hiểu thêm" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "了解更多" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "了解更多" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "了解更多" } } } }, "Less" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Weniger" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Menos" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Moins" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Kurang" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Meno" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "少ない" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "적음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Mniej" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Menos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Menos" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Меньше" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Menej" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Manj" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Daha az" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Менше" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ít hơn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "更少" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "少" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "更少" } } } }, "Link all selected items to the chosen app" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Verknüpfen Sie alle ausgewählten Elemente mit der gewählten App" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Vincular todos los elementos seleccionados a la aplicación elegida" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Lier tous les éléments sélectionnés à l'application choisie" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tautkan semua item yang dipilih ke aplikasi yang dipilih" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Collega tutti gli elementi selezionati all'app scelta" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "選択したすべてのアイテムを選択したアプリにリンクする" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "선택한 모든 항목을 선택한 앱에 연결" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Połącz wszystkie wybrane elementy z wybraną aplikacją" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Vincular todos os itens selecionados ao aplicativo escolhido" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ligar todos os itens selecionados à aplicação escolhida" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Связать все выбранные элементы с выбранным приложением" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Prepojiť všetky vybrané položky s vybranou aplikáciou" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Poveži vse izbrane elemente z izbrano aplikacijo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Seçilen tüm öğeleri seçilen uygulamaya bağla" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Зв'язати всі вибрані елементи з обраним додатком" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Liên kết tất cả các mục đã chọn với ứng dụng đã chọn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "将所有选定项目链接到所选应用程序" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "將所有選定項目連結到所選應用程式" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "將所有選定項目連結到所選應用程式" } } } }, "Link all selected items to the chosen app and remove from orphan scans." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Verknüpfen Sie alle ausgewählten Elemente mit der gewählten App und entfernen Sie sie aus den verwaisten Scans." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Vincular todos los elementos seleccionados a la aplicación elegida y eliminar de los escaneos huérfanos." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Lier tous les éléments sélectionnés à l'application choisie et les supprimer des analyses orphelines." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tautkan semua item yang dipilih ke aplikasi yang dipilih dan hapus dari pemindaian yatim." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Collega tutti gli elementi selezionati all'app scelta e rimuovili dalle scansioni orfane." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "選択したすべてのアイテムを選択したアプリにリンクし、孤立したスキャンから削除します。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "선택한 모든 항목을 선택한 앱에 연결하고 연결되지 않은 항목 검색에서 제거합니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Połącz wszystkie wybrane elementy z wybraną aplikacją i usuń je z osieroconych skanów." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Vincular todos os itens selecionados ao aplicativo escolhido e remover das verificações órfãs." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ligar todos os itens selecionados à aplicação escolhida e remover das análises órfãs." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Привязать все выбранные элементы к выбранному приложению и исключить из сканирования оставшихся." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Prepojiť všetky vybrané položky s vybranou aplikáciou a odstrániť zo sirotích skenov." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Povežite vse izbrane elemente z izbrano aplikacijo in jih odstranite iz iskanj osirotelih datotek." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Seçilen tüm öğeleri seçilen uygulamaya bağlayın ve yetim taramalardan kaldırın." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Зв'язати всі вибрані елементи з обраним додатком і видалити з сирітських сканів." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Liên kết tất cả các mục đã chọn với ứng dụng đã chọn và xóa khỏi các lần quét mồ côi." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "将所有选定项目链接到所选应用程序并从孤立扫描中移除" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "將所有選定項目連結到所選應用程式並從孤立掃描中移除" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "將所有選定項目連結到所選應用程式並從孤立掃描中移除" } } } }, "Link Selected to App" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Auswahl mit App verknüpfen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Vincular seleccionado a la aplicación" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Lier la sélection à l'application" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tautkan yang Dipilih ke Aplikasi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Collega selezionato all'app" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "選択したリンクをアプリに" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "선택 항목을 앱에 연결" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Połącz wybrane do aplikacji" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Vincular Selecionado ao Aplicativo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ligar Selecionado à Aplicação" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Связать выбранное с приложением" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Prepojiť vybrané s aplikáciou" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Poveži izbrano z aplikacijo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Seçilenleri Uygulamaya Bağla" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Зв'язати вибране з додатком" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Liên kết đã chọn với ứng dụng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "链接选定到应用程序" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "連結選擇至應用程式" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "連結選定至應用程式" } } } }, "Link To" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Link zu" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Enlazar a" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Lien vers" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tautkan Ke" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Collega a" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "リンク先" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "링크 연결" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Połącz do" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Linkar Para" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Link Para" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Связать с" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odkaz Na" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Povezava na" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Şuna Bağla" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Посилання на" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Liên kết đến" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "链接到" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "連結至" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "連結至" } } } }, "Lipo" : { "comment" : "Arch liposuction", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Lipo" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Lipo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Lipo" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lipo" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Lipo" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Lipo" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "번들 축소" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Lipo" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Lipo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Lipo" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Липо" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Kôš" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Lipo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Lipo" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Lipo" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Lipo" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "瘦身" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "節省" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "節省" } } } }, "Lipo Architectures" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Lipo Architektur" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Arquitecturas de Lipo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Lipo Architectures" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Arsitektur Lipo" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Architetture Lipo" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Lipoアーキテクチャ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "제거할 아키텍처" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Architektury Lipo" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Arquiteturas Lipo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Arquiteturas Lipo" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Архитектуры Lipo" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Lipo architektúry" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Lipo Arhitekture" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Lipo Mimarileri" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Архітектури Lipo" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Kiến trúc Lipo" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "瘦身架构" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "節省應用程式架構" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "節省應用程式架構" } } } }, "List View" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Listenansicht" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Vista de lista" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Vue en liste" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tampilan Daftar" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Vista elenco" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "リストビュー" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "목록 보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Widok listy" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Exibição em Lista" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Vista de Lista" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Представление списка" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobrazenie zoznamu" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pogled seznama" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Liste Görünümü" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Перегляд списку" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chế độ xem danh sách" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "列表视图" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "清單檢視" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "列表檢視" } } } }, "Load app updates on startup" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "App-Updates beim Start laden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cargar actualizaciones de la aplicación al iniciar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Charger les mises à jour de l'application au démarrage" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Muat pembaruan aplikasi saat startup" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Carica gli aggiornamenti dell'app all'avvio" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "起動時にアプリの更新を読み込む" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "시작 시 앱 업데이트 로드" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ładuj aktualizacje aplikacji przy uruchomieniu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Carregar atualizações do aplicativo na inicialização" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Carregar atualizações da aplicação ao iniciar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Загружать обновления приложения при запуске" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Načítať aktualizácie aplikácie pri spustení" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Naloži posodobitve aplikacije ob zagonu" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Başlangıçta uygulama güncellemelerini yükle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Завантажувати оновлення програми під час запуску" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tải cập nhật ứng dụng khi khởi động" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "启动时加载应用更新" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "啟動時載入應用更新" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "啟動時加載應用程式更新" } } } }, "Load the service" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dienst laden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cargar el servicio" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Charger le service" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Muat layanan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Carica il servizio" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "サービスを読み込む" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "서비스 로드" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Załaduj usługę" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Carregar o serviço" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Carregar o serviço" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Загрузить службу" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Načítať službu" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Naloži storitev" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hizmeti yükle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Завантажити сервіс" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tải dịch vụ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "加载服务" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "加載服務" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "載入服務" } } } }, "Loading files..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dateien werden geladen..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cargando archivos..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Chargement de fichiers…" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Memuat file..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Caricamento file..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "読み込んでいます..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "파일 로딩 중..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ładowanie plików…" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Carregando arquivos..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A carregar ficheiros..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Загрузка файлов..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Načítavam súbory..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Nalaganje datotek..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Dosyalar yükleniyor..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Завантаження файлів..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang tải tệp tin..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "正在加载文件..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "正在載入檔案..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "正在載入檔案..." } } } }, "Loading languages..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Sprachen werden geladen..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cargando idiomas..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Chargement des langues..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Memuat bahasa..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Caricamento delle lingue..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "言語を読み込んでいます..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "언어 로드 중..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ładowanie języków..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Carregando idiomas..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A carregar idiomas..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Загрузка языков..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Načítavam jazyky..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Nalagam jezike..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Diller yükleniyor..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Завантаження мов..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang tải ngôn ngữ..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "加载语言..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "載入語言..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "載入語言中..." } } } }, "Loading launch services..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Startdienste werden geladen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cargando servicios de lanzamiento..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Chargement des services de lancement..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Memuat layanan peluncuran..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Caricamento dei servizi di avvio..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "起動サービスを読み込んでいます..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "실행 서비스 로딩 중..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ładowanie usług startowych..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Carregando serviços de inicialização..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A carregar serviços de lançamento..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Загрузка служб запуска..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Načítanie služieb spúšťania..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Nalaganje zagonskih storitev..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Başlatma servisleri yükleniyor..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Завантаження служб запуску..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang tải dịch vụ khởi động..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "正在加载启动服务" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "載入啟動服務中..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "正在載入啟動服務" } } } }, "Loading package details..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Lade Paketdetails..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cargando detalles del paquete..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Chargement des détails du paquet..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Memuat detail paket..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Caricamento dettagli del pacchetto..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "パッケージの詳細を読み込んでいます..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "패키지 세부정보 로딩 중..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ładowanie szczegółów pakietu..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Carregando detalhes do pacote..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Carregando detalhes do pacote..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Загрузка сведений о пакете..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Načítavam podrobnosti balíka..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Nalaganje podrobnosti paketa..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Paket detayları yükleniyor..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Завантаження деталей пакета..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang tải chi tiết gói..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "正在加载包详情..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "正在載入包詳細資訊..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "正在載入包詳細資訊..." } } } }, "Loading package files..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Paketdateien werden geladen..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cargando archivos del paquete..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Chargement des fichiers du paquet..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Memuat file paket..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Caricamento dei file del pacchetto..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "パッケージファイルを読み込んでいます..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "패키지 파일 로드 중..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ładowanie plików pakietu..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Carregando arquivos do pacote..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A carregar ficheiros de pacotes..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Загрузка файлов пакета..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Načítanie súborov balíka..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Nalaganje datotek paketa..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Paket dosyaları yükleniyor..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Завантаження файлів пакета..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang tải tệp gói..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "正在加载包文件..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "正在載入套件檔案..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "正在載入套件檔案..." } } } }, "Loading packages..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Pakete werden geladen..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cargando paquetes..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Chargement des paquets..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Memuat paket..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Caricamento pacchetti..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "パッケージを読み込んでいます…" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "패키지를 로드하는 중..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ładowanie pakietów..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Carregando pacotes..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A carregar pacotes..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Загрузка пакетов..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Načítavanie balíkov..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Nalaganje paketov..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Paketler yükleniyor..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Завантаження пакетів..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang tải gói..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "正在加载软件包..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "正在載入套件..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "載入套件中..." } } } }, "Loading permissions..." : { }, "Loading plugins..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Plugins werden geladen..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cargando plugins..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Chargement des plugins..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Memuat plugin..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Caricamento plugin..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "プラグインを読み込んでいます..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "플러그인 로딩 중..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ładowanie wtyczek..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Carregando plugins..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Carregando plugins..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Загрузка плагинов..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Načítavam pluginy..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Nalaganje vtičnikov..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Eklentiler yükleniyor..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Завантаження плагінів..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang tải plugin..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "加载插件中..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "載入插件中..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "載入插件中..." } } } }, "Loading taps..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Lade Wasserhähne..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cargando grifos..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Chargement des robinets..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Memuat keran..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Caricamento rubinetti..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "タップを読み込んでいます..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "탭 로딩 중..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ładowanie kranów..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Carregando torneiras..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A carregar toques..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Загрузка кранов..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Načítavajú sa kohútiky..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Nalaganje tapov..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Musluklar yükleniyor..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Завантаження кранів..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang tải vòi..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "加载水龙头..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "加載水龍頭..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "加載水龍頭..." } } } }, "Loading..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Laden..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cargando..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Chargement..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Memuat..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Caricamento..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "読み込み中..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "로딩 중..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ładowanie..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Carregando..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A carregar..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Загрузка..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Načítava sa..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Nalaganje..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yükleniyor..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Завантаження..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang tải..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "加载中..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "載入中..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "載入中..." } } } }, "Location:" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ort:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ubicación:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Emplacement:" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lokasi:" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Posizione:" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "場所:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "위치:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Lokalizacja:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Localização:" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Localização:" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Расположение:" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Poloha:" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Lokacija:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Konum:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Розташування:" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Vị trí:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "位置:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "位置:" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "位置:" } } } }, "Locations that will be searched for .app files. Click a non-default path to remove it. Default paths can't be removed." : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Orte in denen nach .app-Dateien gesucht werden soll. Klicken Sie auf einen Nicht-Standardpfad, um ihn zu entfernen. Standardpfade können nicht entfernt werden." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ubicaciones que se buscarán para archivos .app. Haz clic en una ruta no predeterminada para eliminarla. Las rutas predeterminadas no se pueden eliminar." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Emplacements dans lesquels les fichiers .app seront recherchés. Cliquez sur un chemin d’accès autre que celui par défaut pour le supprimer. Les chemins par défaut ne peuvent pas être supprimés." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lokasi yang akan dicari untuk file .app. Klik jalur non-default untuk menghapusnya. Jalur default tidak dapat dihapus." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Posizioni che verranno cercate per i file .app. Clicca su un percorso non predefinito per rimuoverlo. I percorsi predefiniti non possono essere rimossi." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アプリケーションファイルの場所を検索します。デフォルト以外のパスをクリックして削除します。デフォルトのパスは削除できません。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : ".app 파일을 검색할 위치입니다. 기본이 아닌 경로를 클릭하여 제거할 수 있습니다. 기본 경로는 제거할 수 없습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Lokalizacje, w których będą wyszukiwane pliki .app. Kliknij ścieżkę inną niż domyślna, aby ją usunąć. Ścieżek domyślnych nie można usuwać." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Locais que serão pesquisados para arquivos .app. Clique em um caminho não padrão para removê-lo. Caminhos padrão não podem ser removidos." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Locais que serão pesquisados para arquivos .app. Clique num caminho não padrão para removê-lo. Caminhos padrão não podem ser removidos." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Места, в которых будет производиться поиск файлов .app. Щёлкните, чтобы выбрать путь, который не соответствует по умолчанию, чтобы удалить его. Пути по умолчанию удалить невозможно." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Umiestnenia, v ktorých sa budú vyhľadávať .app súbory. Kliknutím na inú ako predvolenú cestu ju odstrániš. Predvolené cesty nie je možné odstrániť." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Lokacije, ki bodo preiskane za .app datoteke. Kliknite na neprivzeto pot, da jo odstranite. Privzetih poti ni mogoče odstraniti." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : ".app dosyaları için aranacak lokasyonlar. Varsayılan olmayan bir dizini kaldırmak için tıklayın. Varsayılan dizinler kaldırılamaz." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Локації, в яких буде здійснюватися пошук файлів .app. Клацніть нестандартний шлях, щоб видалити його. Шляхи за замовчуванням не можна видалити." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Các vị trí sẽ được tìm kiếm để tìm tệp .app. Nhấp vào đường dẫn không mặc định để loại bỏ. Các đường dẫn mặc định không thể loại bỏ được." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "将搜索 .app 文件的位置。点击非默认路径以移除。默认路径无法移除。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "將會搜尋 .app 檔案的位置。按一下非預設的路徑以移除它。預設路徑無法移除。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "將會搜尋 .app 檔案的位置。按一下非預設的路徑以移除它。預設路徑無法移除。" } } } }, "Logical" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Logisch" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Lógico" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Logique" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Logis" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Logico" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "論理サイズ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "논리적" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Logiczne" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Lógico" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Lógico" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Логически" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Logická" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Logično" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Mantıksal" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Логічно" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Kích thước logic (Logical type)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "逻辑大小" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "邏輯大小" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "邏輯大小" } } } }, "Login Items" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Anmeldeobjekte" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Elementos de inicio de sesión" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Éléments de connexion" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Item Login" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Elementi di accesso" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ログイン項目" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "로그인 항목" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Rzeczy otwierane podczas logowania" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Itens de Login" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Itens de Login" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Элементы загрузки" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Položky prihlásenia" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prijavni elementi" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Oturum Açma Öğeleri" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Елементи Логіну" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Các mục khởi động" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "登录项" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "登入項目" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "登入項目" } } } }, "macOS only enables extensions if the main app is in Applications folder" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "MacOS aktiviert Erweiterungen nur, wenn sich die Hauptanwendung im Ordner „Programme“ befindet." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "macOS solo habilita extensiones si la aplicación principal está en la carpeta Aplicaciones" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "macOS n'active les extensions que si l'application principale se trouve dans le dossier Applications" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "macOS hanya mengaktifkan ekstensi jika aplikasi utama ada di folder Aplikasi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "macOS abilita le estensioni solo se l'app principale si trova nella cartella Applicazioni" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "macOS はアプリケーションフォルダに本体アプリがある場合のみ拡張機能を有効にします" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "macOS는 메인 앱이 응용 프로그램 폴더에 있을 때만 확장 기능을 활성화합니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "System macOS włącza rozszerzenia tylko wtedy, gdy główna aplikacja znajduje się w folderze Aplikacje" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "o macOS só habilita extensões se o aplicativo principal estiver na pasta Aplicativos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "O macOS só ativa extensões se a aplicação principal estiver na pasta Aplicações" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "macOS включает расширения, только если основное приложение находится в папке \"Программы\"" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "macOS povolí rozšírenia len v prípade, ak je hlavná aplikácia v priečinku Aplikácie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "macOS omogoči razširitve le, če je glavna aplikacija v mapi Applications" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "macOS, uzantıları yalnızca ana uygulama Uygulamalar klasöründe ise etkinleştirir" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "macOS дозволяє розширення, тільки якщо основна програма знаходиться в папці Applications" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "macOS chỉ bật các tiện ích mở rộng nếu ứng dụng chính nằm trong thư mục Ứng dụng (Applications)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "macOS仅当主应用程序位于应用程序文件夹中时才启用扩展" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "只有主應用程式位於「應用程式」資料夾時,macOS 才會啟用延伸功能" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "只有主應用程式位於「應用程式」資料夾時,macOS 才會啟用延伸功能" } } } }, "Made with ❤️ by Alin Lupascu" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Hergestellt mit ❤️ von Alin Lupascu" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Hecho con ❤️ por Alin Lupascu" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Fait avec ❤️ par Alin Lupascu" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Dibuat dengan ❤️ oleh Alin Lupascu" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Fatto con ❤️ da Alin Lupascu" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Alin Lupascu による ❤️ 作品" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Alin Lupascu의 ❤️로 제작" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Stworzone z ❤️ przez Alin Lupascu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Feito com ❤️ por Alin Lupascu" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Feito com ❤️ por Alin Lupascu" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Сделано с ❤️ от Alin Lupascu" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vytvoril s ❤️ Alin Lupascu" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Narejeno z ❤️ , avtor Alin Lupascu" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Alin Lupascu tarafından ❤️ ile yapıldı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Зроблено з ❤️ Alin Lupascu" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Được tạo với ❤️ bởi Alin Lupascu" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "由 Alin Lupascu 用 ❤️ 制作" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "由 Alin Lupascu 用 ❤️ 製作" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "由 Alin Lupascu 用 ❤️ 製作" } } } }, "Manage Homebrew packages, taps, and maintenance" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Verwalten von Homebrew-Paketen, Taps und Wartung" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Gestionar paquetes, taps y mantenimiento de Homebrew" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Gérer les paquets Homebrew, les taps et la maintenance" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Kelola paket, tap, dan pemeliharaan Homebrew" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Gestisci pacchetti Homebrew, tap e manutenzione" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew パッケージ、タップ、およびメンテナンスの管理" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 패키지, 탭 및 유지 관리 관리" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zarządzaj pakietami Homebrew, kranami i konserwacją" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Gerenciar pacotes, taps e manutenção do Homebrew" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Gerir pacotes Homebrew, taps e manutenção" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Управление пакетами Homebrew, кранами и обслуживанием" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Spravovať balíčky Homebrew, kohútiky a údržbu" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Upravljaj Homebrew pakete, pipe in vzdrževanje" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew paketlerini, musluklarını ve bakımını yönetin" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Керування пакетами Homebrew, кранами та обслуговуванням" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Quản lý các gói Homebrew, taps và bảo trì" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "管理 Homebrew 包、tap 和维护" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "管理 Homebrew 套件、tap 和維護" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "管理 Homebrew 套件、tap 和維護" } } } }, "Manage launch agents, daemons, and XPC services" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Startagenten, Daemons und XPC-Dienste verwalten" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Administrar agentes de lanzamiento, demonios y servicios XPC" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Gérer les agents de lancement, les démons et les services XPC" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Kelola agen peluncuran, daemon, dan layanan XPC" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Gestisci agenti di lancio, demoni e servizi XPC" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "起動エージェント、デーモン、およびXPCサービスを管理する" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "LaunchAgent, 데몬 및 XPC 서비스 관리" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zarządzaj agentami uruchamiania, demonami i usługami XPC" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Gerenciar agentes de inicialização, daemons e serviços XPC" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Gerir agentes de lançamento, daemons e serviços XPC" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Управление агентами запуска, демонами и XPC-сервисами" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Spravujte spúšťacie agenty, démonov a služby XPC" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Upravljanje zagonskih agentov, demonov in XPC storitev" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Başlangıç ajanlarını, daemons ve XPC servislerini yönetin" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Керувати запусковими агентами, демонами та сервісами XPC" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Quản lý tác nhân khởi động, dịch vụ nền, và dịch vụ XPC" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "管理启动代理、守护程序和XPC服务" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "管理啟動代理、守護程式和XPC服務" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "管理啟動代理、守護程式和 XPC 服務" } } } }, "Manage packages installed via macOS Installer" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Pakete verwalten, die über den macOS Installer installiert wurden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Administrar paquetes instalados a través del Instalador de macOS" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Gérer les packages installés via macOS Installer" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Kelola paket yang diinstal melalui Penginstal macOS" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Gestisci i pacchetti installati tramite macOS Installer" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "macOSインストーラーを通じてインストールされたパッケージを管理する" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "macOS 설치 관리자를 통해 설치된 패키지 관리" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zarządzaj pakietami zainstalowanymi za pomocą instalatora macOS" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Gerenciar pacotes instalados via Instalador do macOS" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Gerir pacotes instalados via Instalador do macOS" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Управление пакетами, установленными через macOS Installer" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Spravujte balíky nainštalované cez Inštalátor macOS" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Upravljaj pakete nameščene prek macOS Installerja" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "macOS Yükleyici ile kurulan paketleri yönet" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Управляйте пакетами, встановленими через інсталятор macOS" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Quản lý các gói cài đặt qua macOS Installer" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "管理通过macOS安装程序安装的包" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "管理通過macOS安裝程式安裝的套件" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "管理透過 macOS 安裝程式安裝的套件" } } } }, "Manage PKG receipts and installations" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Verwalten Sie PKG-Belege und Installationen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Gestionar recibos y instalaciones de PKG" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Gérer les reçus et installations PKG" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Kelola tanda terima dan instalasi PKG" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Gestisci le ricevute e le installazioni PKG" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "PKGの受領書とインストールを管理" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "PKG 영수증 및 설치 관리" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zarządzaj paragonami i instalacjami PKG" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Gerenciar recibos e instalações PKG" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Gerir recibos e instalações de PKG" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Управление квитанциями и установками PKG" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Spravujte potvrdenia a inštalácie PKG" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Upravljaj s prejemki in namestitvami PKG" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "PKG makbuzlarını ve kurulumlarını yönetin" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Керування квитанціями та встановленнями PKG" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Quản lý biên lai và cài đặt PKG" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "管理 PKG 收据和安装" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "管理 PKG 收據和安裝" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "管理 PKG 收據和安裝" } } } }, "Manage third-party plugins and extensions" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Verwalten von Plugins und Erweiterungen von Drittanbietern" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Gestionar complementos y extensiones de terceros" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Gérer les plugins et extensions tiers" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Kelola plugin dan ekstensi pihak ketiga" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Gestisci plugin ed estensioni di terze parti" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "サードパーティのプラグインと拡張機能を管理" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "타사 플러그인 및 확장 관리" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zarządzaj wtyczkami i rozszerzeniami innych firm" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Gerenciar plugins e extensões de terceiros" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Gerir plugins e extensões de terceiros" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Управление сторонними плагинами и расширениями" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Spravovať doplnky a rozšírenia tretích strán" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Upravljanje vtičnikov in razširitev tretjih oseb" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Üçüncü taraf eklentileri ve uzantıları yönetin" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Керування сторонніми плагінами та розширеннями" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Quản lý plugin và tiện ích mở rộng của bên thứ ba" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "管理第三方插件和扩展" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "管理第三方插件和擴展" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "管理第三方插件和擴展" } } } }, "Management" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Management" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Gestión" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Gestion" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Manajemen" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Gestione" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "管理" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "관리" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zarządzanie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Gerenciamento" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Gestão" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Управление" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Správa" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Upravljanje" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yönetme" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Керування" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Quản lý" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "管理" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "管理" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "管理" } } } }, "Manual Entry" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Manuelle Eingabe" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Entrada Manual" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Saisie manuelle" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Entri Manual" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Inserimento Manuale" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "手動入力" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "수동 입력" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wpis ręczny" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Entrada Manual" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Entrada Manual" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Ручной ввод" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Manuálny vstup" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ročni vnos" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Manuel Giriş" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ручний ввід" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Nhập Thủ Công" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "手动输入" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "手動輸入" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "手動輸入" } } } }, "Manually hide an app from update checks" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Eine App manuell von Update-Überprüfungen ausblenden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ocultar manualmente una aplicación de las comprobaciones de actualización" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Masquer manuellement une application des vérifications de mise à jour" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Sembunyikan aplikasi secara manual dari pemeriksaan pembaruan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nascondi manualmente un'app dai controlli di aggiornamento" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "手動でアプリを更新チェックから隠す" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "업데이트 확인에서 앱 수동으로 숨기기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ręcznie ukryj aplikację przed sprawdzaniem aktualizacji" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ocultar manualmente um aplicativo das verificações de atualização" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ocultar manualmente uma aplicação das verificações de atualização" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Вручную скрыть приложение от проверки обновлений" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Ručne skryť aplikáciu pred kontrolami aktualizácií" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ročno skrij aplikacijo pred preverjanjem posodobitev" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Güncelleme kontrollerinden bir uygulamayı manuel olarak gizle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Вручну приховати додаток від перевірок оновлень" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ẩn thủ công một ứng dụng khỏi kiểm tra cập nhật" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "手动隐藏应用以免更新检查" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "手動隱藏應用程式以避免更新檢查" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "手動隱藏應用程式以避免更新檢查" } } } }, "match confidence" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Übereinstimmungsgenauigkeit" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "confianza de coincidencia" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "confiance de correspondance" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "keyakinan kecocokan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "fiducia di corrispondenza" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "一致信頼度" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "일치 신뢰도" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "pewność dopasowania" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "confiança de correspondência" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "confiança de correspondência" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "уверенность в совпадении" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "zhoda dôvery" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "ujemanje zaupanja" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "eşleşme güveni" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "відповідність довіри" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "độ tin cậy khớp" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "匹配信心" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "匹配信心" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "匹配信心" } } } }, "Max Value" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Maximalwert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Valor Máximo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Valeur maximale" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Nilai Maksimum" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Valore massimo" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "最大値" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "최대 값" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Maksymalna wartość" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Valor Máximo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Valor Máximo" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Максимальное значение" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Maximálna hodnota" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Največja vrednost" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Maksimum Değer" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Максимальне значення" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Giá trị tối đa" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "最大值" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "最大值" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "最大值" } } } }, "MB" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "MB" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "MB" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mo" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "MB" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "MB" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "MB" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "MB" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "MB" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "MB" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "MB" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "МБ" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "MB" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "MB" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "MB" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "МБ" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "MB" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "MB" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "MB" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "MB" } } } }, "Missing Permissions" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Fehlende Berechtigungen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Permisos faltantes" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Autorisations manquantes" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Izin Hilang" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Permessi mancanti" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "権限が不足しています" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "권한 누락" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Brakujące uprawnienia" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Permissões Ausentes" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Permissões em falta" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Отсутствуют права" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Chýbajúce povolenia" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Manjkajoča dovoljenja" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Eksik İzinler" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Відсутні Дозволи" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Cần cấp quyền" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "缺少权限" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "欠缺權限" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "欠缺權限" } } } }, "Missing permissions:" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Fehlende Berechtigungen:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Faltan permisos:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Autorisations manquantes :" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Izin yang hilang:" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Permessi mancanti:" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "権限がありません:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "권한 누락:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Brak uprawnień:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Permissões ausentes:" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Permissões em falta:" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Отсутствуют разрешения:" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Chýbajúce povolenia:" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Manjkajoča dovoljenja:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Eksik izinler:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Відсутні дозволи:" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thiếu quyền:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "缺少权限:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "缺少權限:" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "缺少權限:" } } } }, "Modified" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Geändert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Modificado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Modifié" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Diubah" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Modificato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "変更済み" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "수정됨" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zmodyfikowano" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Modificado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Modificado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Изменено" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Upravené" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Spremenjeno" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Değiştirildi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Змінено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đã chỉnh sửa" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "已修改" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "已修改" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "已修改" } } } }, "Modified Date" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Verändert am" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Fecha de modificación" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Date de modification" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tanggal Diubah" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Data di modifica" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "修正日" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "수정 날짜" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Data modyfikacji" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Data de Modificação" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Data de Modificação" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Дата изменения" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Dátum zmeny" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Datum spremembe" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Değiştirme Tarihi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Дата модифікації" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ngày chỉnh sửa" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "修改日期" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "修改日期" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "修改日期" } } } }, "Monthly" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Monatlich" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Mensual" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mensuel" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Bulanan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Mensile" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "毎月" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "월간" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Miesięcznie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Mensal" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Mensalmente" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Ежемесячно" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Mesačne" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Mesečno" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Aylık" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Щомісяця" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hàng tháng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "每月" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "每月" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "每月" } } } }, "More" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Mehr" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Más" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Plus" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lainnya" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Altro" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "もっと見る" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "더보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Więcej" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Mais" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Mais" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Больше" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Viac" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Več" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Daha fazla" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Більше" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thêm" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "更多" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "更多" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "更多" } } } }, "Most files" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Die meisten Dateien" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "La mayoría de los archivos" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "La pluspart des fichiers" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Sebagian besar file" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "La maggior parte dei file" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ほとんどのファイル" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "대부분의 파일" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Większość plików" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "A maioria dos arquivos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A maioria dos arquivos" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Большинство файлов" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Väčšina súborov" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Večina datotek" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Çoğu dosya" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Більшість файлів" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hầu hết các tệp" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "大多数文件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "大多數檔案" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "大部分檔案" } } } }, "Move to Trash" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "In den Papierkorb verschieben" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Mover a la papelera" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Déplacer vers la corbeille" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pindahkan ke Sampah" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Sposta nel cestino" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ゴミ箱に移動" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "휴지통으로 이동" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Przenieś do kosza" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Mover para a lixeira" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Mover para o Lixo" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Переместить в корзину" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Presunúť do koša" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Premakni v smeti" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Çöpe Taşı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Перемістити в кошик" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chuyển vào Thùng rác" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "移至垃圾桶" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "移至垃圾桶" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "移至垃圾桶" } } } }, "Multi-select disabled in sidebar app list" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Mehrfachauswahl in der Seitenleisten-App-Liste deaktiviert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Selección múltiple desactivada en la lista de aplicaciones de la barra lateral" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Sélection multiple désactivée dans la liste des applications de la barre latérale" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Multi-pilih dinonaktifkan dalam daftar aplikasi sidebar" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Selezione multipla disabilitata nell'elenco delle app della barra laterale" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "サイドバーのアプリ一覧で複数選択が無効化されています" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "사이드바 앱 목록에서 다중 선택 비활성화" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wybór wielu elementów wyłączony na liście aplikacji w pasku bocznym" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Seleção múltipla desativada na lista de aplicativos da barra lateral" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Seleção múltipla desativada na lista de apps da barra lateral" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Выбор нескольких элементов отключен в списке приложений боковой панели" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zvýber viacerých možností zakázaný v zozname aplikácií na bočnom paneli" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Večkratni izbor onemogočen na seznamu aplikacij v stranski vrstici" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kenar çubuğu uygulama listesinde çoklu seçim devre dışı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Відключено мультивибір у списку застосунків на бічній панелі" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đa chọn bị vô hiệu hóa trong danh sách ứng dụng ở thanh bên" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "侧边栏应用列表中禁用多选" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "側邊欄應用程式列表中的多選功能已禁用" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "在側邊欄應用程式列表中禁用多選功能" } } } }, "Multi-select enabled in sidebar app list" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Mehrfachauswahl in der Seitenleiste-App-Liste aktiviert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Selección múltiple activada en la lista de aplicaciones de la barra lateral" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Sélection multiple activée dans la liste des applications de la barre latérale" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Multi-pilih diaktifkan dalam daftar aplikasi sidebar" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Selezione multipla abilitata nell'elenco delle app della barra laterale" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "サイドバーのアプリリストで複数選択が有効" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "사이드바 앱 목록에서 다중 선택 활성화" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wybór wielu elementów włączony na liście aplikacji w pasku bocznym" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Seleção múltipla habilitada na lista de aplicativos da barra lateral" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Seleção múltipla ativada na lista de aplicativos da barra lateral" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "В боковой панели списка приложений включен выбор нескольких элементов" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Povolený multi-výber v zozname aplikácií na bočnom paneli" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Večkratni izbor omogočen na seznamu aplikacij v stranski vrstici" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kenar çubuğu uygulama listesinde çoklu seçim etkinleştirildi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Увімкнено можливість багатократного вибору в списку додатків на бічній панелі" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đã bật chọn nhiều trong danh sách ứng dụng thanh bên" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "在侧边栏应用列表中启用多选" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "側邊欄應用程式列表已啟用多選功能" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "側邊欄應用程式列表中啟用多重選擇" } } } }, "Name" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Name" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Nombre" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Nom" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Nama" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nome" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "名前" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이름" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nazwa" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nome" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Nome" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Название" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Názov" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ime" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İsim" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Назва" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tên" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "名称" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "名稱" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "名稱" } } } }, "Name: %@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Name: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Nombre: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Nom: %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Nama: %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nome: %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "名前: %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이름: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nazwa: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nome: %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Nome: %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Имя: %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Názov: %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ime: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Adı: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ім'я: %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tên: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "名称:%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "名稱:%@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "名稱: %@" } } } }, "Navigate To" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Navigiere zu" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Navegar a" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Accéder à" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Navigasi Ke" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Naviga verso" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "移動先" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이동" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Przejdź do" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Navegar Para" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Navegar Para" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Перейти к" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Prejsť na" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pojdi na" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Şuraya Git:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Перейти" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đi đến" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "导航至" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "導覽到" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "導覽到" } } } }, "Never" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Nie" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Nunca" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Jamais" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak pernah" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Mai" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "しない" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "절대" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nigdy" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nunca" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Nunca" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Никогда" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nikdy" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Nikoli" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hiçbir zaman" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ніколи" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không bao giờ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "从不" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "永不" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "永不" } } } }, "New Announcement" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Neue Ankündigung" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Nuevo anuncio" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Nouvelle annonce" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pengumuman Baru" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nuovo annuncio" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "新しいお知らせ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "새로운 공지" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nowe ogłoszenie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Novo Anúncio" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Novo Anúncio" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Новое объявление" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nové oznámenie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Nova objava" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yeni Duyuru" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Нове Оголошення" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thông báo mới" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "新公告" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "新宣佈" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "新宣佈" } } } }, "New Feature!" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Neue Funktion!" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "¡Nueva Función!" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Nouvelle fonctionnalité !" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Fitur Baru!" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nuova funzionalità!" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "新機能!" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "새로운 기능!" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nowa funkcja!" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Novo recurso!" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Nova Funcionalidade!" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Новая функция!" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nová funkcia!" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Nova funkcija!" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yeni Özellik!" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Нова функція!" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tính năng mới!" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "新功能!" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "新功能!" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "新功能!" } } } }, "Next update check: %@" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Nächstes Update am: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Próxima verificación de actualización: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Prochaine vérification de mise à jour : %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pemeriksaan pembaruan berikutnya: %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Prossimo controllo aggiornamenti: %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "次のアップデートチェック:%@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "다음 업데이트 확인: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Następna aktualizacja: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Próxima verificação de atualização: %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Próxima verificação de atualização: %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Следующая проверка обновления: %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Ďalšia kontrola aktualizácie: %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Naslednji pregled posodobitev: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sonraki güncelleme kontrolü: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Наступна перевірка оновлення: %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Lần kiểm tra cập nhật tiếp theo: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "下次更新检查:%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "下一個更新檢查:%@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "下一個更新檢查:%@" } } } }, "No apps available for thinning" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Keine Apps zum Ausdünnen verfügbar" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No hay aplicaciones disponibles para adelgazar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Aucune application disponible pour l'allègement" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak ada aplikasi yang tersedia untuk penipisan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nessuna app disponibile per il thinning" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "スリム化のためのアプリはありません" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "간소화할 수 있는 앱이 없습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Brak aplikacji do zmniejszenia rozmiaru" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum aplicativo disponível para redução" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Nenhuma aplicação disponível para redução" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Нет приложений, доступных для оптимизации" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nie sú k dispozícii žiadne aplikácie na zmenšenie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ni aplikacij na voljo za redčenje" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İnceltilmek için uygun uygulama yok" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Немає доступних додатків для оптимізації" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không có ứng dụng nào có sẵn để giảm kích thước" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "没有可用于瘦身的应用程序" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "沒有可用於精簡的應用程式" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "沒有可供精簡的應用程式" } } } }, "No apps excluded" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Keine Apps ausgeschlossen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No hay aplicaciones excluidas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Aucune application exclue" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak ada aplikasi yang dikecualikan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nessuna app esclusa" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "除外されたアプリはありません" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "제외된 앱 없음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Brak wykluczonych aplikacji" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum aplicativo excluído" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Nenhuma aplicação excluída" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Приложения не исключены" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Žiadne aplikácie vylúčené" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Nobena aplikacija ni izključena" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hariç tutulan uygulama yok" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Жодні додатки не виключені" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không có ứng dụng nào bị loại trừ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "没有应用被排除" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "沒有應用程式被排除" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "沒有應用程式被排除" } } } }, "No apps found" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Keine Apps gefunden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No se encontraron aplicaciones" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Aucune application trouvée" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak ada aplikasi yang ditemukan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nessuna app trovata" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アプリが見つかりません" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "앱을 찾을 수 없습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nie znaleziono aplikacji" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum aplicativo encontrado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Nenhuma aplicação encontrada" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Приложения не найдены" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Žiadne aplikácie nenájdené" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ni najdenih aplikacij" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uygulama bulunamadı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Додатки не знайдено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không tìm thấy ứng dụng nào" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "未找到应用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "找不到應用程式" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "找不到應用程式" } } } }, "No Changelog" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Kein Änderungsprotokoll" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Sin registro de cambios" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Pas de journal des modifications" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak ada log perubahan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nessun changelog" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "変更履歴なし" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "변경 로그 없음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Brak dziennika zmian" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Sem registro de alterações" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Sem registo de alterações" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Нет журнала изменений" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Žiadny záznam zmien" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ni dnevnika sprememb" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Değişiklik günlüğü yok" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Немає журналу змін" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không có nhật ký thay đổi" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "没有更改日志" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "沒有更改日誌" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "沒有更改記錄" } } } }, "No delete history" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Kein Löschverlauf" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Sin historial de eliminación" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Aucun historique de suppression" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak ada riwayat penghapusan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nessuna cronologia di eliminazione" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "削除履歴なし" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "삭제 기록 없음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Brak historii usuwania" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Sem histórico de exclusão" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Sem histórico de eliminação" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Нет истории удаления" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Žiadna história odstránenia" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ni zgodovine brisanja" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Silme geçmişi yok" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Немає історії видалення" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không có lịch sử xóa" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "无删除历史" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "無刪除歷史" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "無刪除歷史" } } } }, "No files or folders added" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Keine Dateien oder Ordner hinzugefügt" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No se agregaron archivos ni carpetas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Aucun fichier ou dossier ajouté" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak ada file atau folder yang ditambahkan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nessun file o cartella aggiunto" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ファイルやフォルダーが追加されていません" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "파일이나 폴더가 추가되지 않았습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nie dodano żadnych plików ani folderów" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum arquivo ou pasta adicionado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum ficheiro ou pasta adicionado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Файлы или папки не выбраны" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Neboli pridané žiadne súbory ani priečinky" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Nobena datoteka ali mapa ni dodana" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Klasör veya dosya eklenmedi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Файли чи папки не додано" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chưa có tệp hoặc thư mục nào được thêm" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "没有添加文件或文件夹" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "沒有加入檔案或資料夾" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "沒有加入檔案或資料夾" } } } }, "No hidden updates" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Keine versteckten Updates" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No hay actualizaciones ocultas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Aucune mise à jour cachée" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak ada pembaruan tersembunyi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nessun aggiornamento nascosto" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "隠しアップデートはありません" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "숨겨진 업데이트 없음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Brak ukrytych aktualizacji" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nenhuma atualização oculta" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Sem atualizações ocultas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Нет скрытых обновлений" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Žiadne skryté aktualizácie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ni skritih posodobitev" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Gizli güncelleme yok" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Немає прихованих оновлень" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không có cập nhật ẩn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "没有隐藏更新" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "沒有隱藏更新" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "沒有隱藏更新" } } } }, "No issues detected" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Keine Probleme festgestellt" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No se detectaron problemas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Aucun problème détecté" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak ada masalah terdeteksi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nessun problema rilevato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "問題は検出されませんでした" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "문제가 발견되지 않음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nie wykryto problemów" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum problema detectado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum problema detectado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Проблем не обнаружено" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Žiadne problémy zistené" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Težave niso zaznane" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sorun tespit edilmedi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Проблем не виявлено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không phát hiện vấn đề nào" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "未检测到问题" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "未檢測到問題" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "未檢測到問題" } } } }, "No items match the current filters" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Keine Elemente entsprechen den aktuellen Filtern" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No hay elementos que coincidan con los filtros actuales" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Aucun élément correspondant au filtre" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak ada item yang sesuai dengan filter saat ini" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nessun elemento corrisponde ai filtri attuali" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "現在のフィルターに一致する項目はありません" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "현재 필터와 일치하는 항목이 없습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Żadne rzeczy nie pasują do aktualnych filtrów" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum item corresponde aos filtros atuais" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum item corresponde aos filtros atuais" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Нет элементов, соответствующих текущим фильтрам" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Žiadne položky nezodpovedajú aktuálnym filtrom" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Nobena postavka ne ustreza trenutnim filtrom" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Geçerli filtrelerle eşleşen öğe yok" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Жоден елемент не відповідає поточним фільтрам" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không có mục nào khớp với bộ lọc hiện tại" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "当前筛选条件下无匹配项" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "目前沒有項目符合篩選條件" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "目前的篩選條件沒有匹配的項目" } } } }, "No launch services found" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Keine Startdienste gefunden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No se encontraron servicios de lanzamiento" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Aucun service de lancement trouvé" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak ada layanan peluncuran yang ditemukan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nessun servizio di avvio trovato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "起動サービスが見つかりません" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "실행 서비스가 발견되지 않음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nie znaleziono żadnych usług związanych z uruchomieniem" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum serviço de inicialização encontrado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum serviço de inicialização encontrado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Службы запуска не найдены" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nenašli sa žiadne spúšťacie služby" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Storitev zagona ni bilo mogoče najti" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Başlangıç hizmeti bulunamadı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Служби запуску не знайдено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không tìm thấy dịch vụ khởi chạy" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "未找到启动服务" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "未找到啟動服務" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "找不到啟動服務" } } } }, "No matching casks found. Try manual entry below." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Keine passenden Fässer gefunden. Versuchen Sie unten die manuelle Eingabe." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No se encontraron barriles coincidentes. Intente ingresar manualmente a continuación." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Aucun fût correspondant trouvé. Essayez une saisie manuelle ci-dessous." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak ada tong yang cocok ditemukan. Coba masukkan secara manual di bawah ini." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nessun barile corrispondente trovato. Prova a inserire manualmente qui sotto." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "一致する樽が見つかりませんでした。以下に手動で入力してみてください。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "일치하는 캐스크를 찾을 수 없습니다. 아래에 수동으로 입력해 보세요." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nie znaleziono pasujących beczek. Spróbuj wprowadzić ręcznie poniżej." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum barril correspondente encontrado. Tente a entrada manual abaixo." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum barril correspondente encontrado. Tente inserir manualmente abaixo." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Соответствующие бочки не найдены. Попробуйте ввести вручную ниже." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nenašli sa žiadne zodpovedajúce sudy. Skúste manuálne zadať nižšie." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ni najdenih ustreznih sodov. Poskusite ročni vnos spodaj." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Eşleşen fıçı bulunamadı. Aşağıdan manuel giriş yapmayı deneyin." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Відповідних бочок не знайдено. Спробуйте ввести вручну нижче." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không tìm thấy thùng phù hợp. Thử nhập thủ công bên dưới." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "找不到匹配的桶。请在下方尝试手动输入。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "找不到匹配的桶。請在下方嘗試手動輸入。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "未找到匹配的桶。請嘗試在下面手動輸入。" } } } }, "No New Announcement" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Keine neuen Ankündigungen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No hay nuevo anuncio" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Pas de nouvelle annonce" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak Ada Pengumuman Baru" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nessun nuovo annuncio" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "新しいお知らせはありません" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "새로운 공지가 없습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Brak nowych ogłoszeń" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum Novo Anúncio" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Sem Novo Anúncio" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Нет новых объявлений" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Žiadne nové oznámenie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ni novih obvestil" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yeni Duyuru Yok" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Нема нових оголошень" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chưa có thông báo nào" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "没有新公告" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "沒有新宣佈" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "沒有新宣佈" } } } }, "No orphaned files found" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Keine verwaisten Dateien gefunden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No se encontraron archivos huérfanos" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Aucun fichier résiduel trouvé" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak ada file yatim piatu yang ditemukan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nessun file orfano trovato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "孤立したファイルは見つかりませんでした" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "연결되지 않은 파일을 찾을 수 없습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nie znaleziono żadnych osieroconych plików" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum arquivo órfão encontrado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Não foram encontrados ficheiros órfãos" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Остаточные файлы не найдены" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nenašli sa žiadne osamotené súbory" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ni najdenih osirotelih datotek" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Başıboş dosya bulunamadı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Осиротілих файлів не знайдено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không có tệp rác nào được tìm thấy" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "未找到孤立文件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "找不到孤立檔案" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "找不到孤立檔案" } } } }, "No orphaned workspaces found" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Keine verwaisten Arbeitsbereiche gefunden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No se encontraron espacios de trabajo huérfanos" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Aucun espace de travail orphelin trouvé" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak ada ruang kerja yatim piatu yang ditemukan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nessun workspace orfano trovato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "孤立したワークスペースは見つかりませんでした" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "연결되지 않은 작업 공간이 없습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nie znaleziono żadnych osieroconych obszarów roboczych" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum espaço de trabalho órfão encontrado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum espaço de trabalho órfão encontrado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Остаточные рабочие пространства не найдены" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nenašli sa žiadne osiřelé pracovné priestory" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ni najdenih osirotelih delovnih prostorov" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Boşta kalan çalışma alanı bulunamadı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Сирітські робочі простори не знайдені" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không tìm thấy không gian làm việc mồ côi" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "未发现孤立的工作区" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "未發現無關聯的工作區" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "找不到孤立的工作區" } } } }, "No packages found" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Keine Pakete gefunden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No se encontraron paquetes" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Aucun paquet trouvé" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak ada paket yang ditemukan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nessun pacchetto trovato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "パッケージが見つかりませんでした" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "패키지를 찾을 수 없음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nie znaleziono żadnych pakietów" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum pacote encontrado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum pacote encontrado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Пакеты не найдены" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Neboli nájdené žiadne balíky" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ni najdenih paketov" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Paket bulunamadı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Пакунки не знайдено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không tìm thấy gói nào" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "未找到软件包" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "找不到套件" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "找不到套件" } } } }, "No packages found in this tap" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Keine Pakete in diesem Tap gefunden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No se encontraron paquetes en este grifo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Aucun paquet trouvé dans ce robinet" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak ada paket yang ditemukan di keran ini" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nessun pacchetto trovato in questo tap" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "このタップにはパッケージが見つかりません" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 탭에서 패키지를 찾을 수 없습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nie znaleziono pakietów w tym kranie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum pacote encontrado nesta torneira" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum pacote encontrado neste tap" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "В этом кране пакетов не найдено" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "V tomto kohútiku sa nenašli žiadne balíky" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "V tem tapu ni najdenih paketov" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu muslukta paket bulunamadı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "У цьому крані пакунків не знайдено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không tìm thấy gói nào trong vòi này" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "此 tap 中未找到包" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "此 tap 中未找到套件" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "此 tap 中未找到套件" } } } }, "No paths excluded or linked to an app" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Keine Pfade ausgeschlossen oder mit einer App verknüpft" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No hay rutas excluidas ni vinculadas a una aplicación" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Aucun chemin exclu ou lié à une application" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak ada jalur yang dikecualikan atau ditautkan ke aplikasi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nessun percorso escluso o collegato a un'app" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アプリに関連付けられたパスや除外されたパスはありません" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "제외되거나 앱에 연결된 경로가 없습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Żadne ścieżki nie są wykluczone ani powiązane z aplikacją" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum caminho excluído ou vinculado a um aplicativo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum caminho excluído ou ligado a uma aplicação" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Пути не исключены и не связаны с приложением" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nie sú vylúčené ani prepojené žiadne cesty s aplikáciou" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ni poti, ki bi bile izključene ali povezane z aplikacijo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hiçbir yol hariç tutulmadı veya bir uygulama ile ilişkilendirilmedi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Жодних шляхів не виключено чи пов'язано з програмою" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không có đường dẫn nào bị loại trừ hoặc liên kết với một ứng dụng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "没有路径被排除或与应用程序关联" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "未排除或未連結到應用程式的路徑" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "沒有路徑被排除或連結到應用程式" } } } }, "No plugins found" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Keine Plugins gefunden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No se encontraron complementos" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Aucun plugin trouvé" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak ada plugin ditemukan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nessun plugin trovato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "プラグインが見つかりません" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "플러그인을 찾을 수 없음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nie znaleziono wtyczek" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum plugin encontrado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum plugin encontrado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Плагины не найдены" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Žiadne doplnky neboli nájdené" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Vtičniki niso najdeni" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Eklenti bulunamadı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Плагіни не знайдено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không tìm thấy plugin nào" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "未找到插件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "未找到插件" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "未找到插件" } } } }, "No related files found" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Keine zugehörigen Dateien gefunden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No se encontraron archivos relacionados" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Aucun fichier associé trouvé" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak ada file terkait ditemukan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nessun file correlato trovato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "関連するファイルが見つかりません" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "관련 파일을 찾을 수 없습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nie znaleziono powiązanych plików" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum arquivo relacionado encontrado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum ficheiro relacionado encontrado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Связанные файлы не найдены" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nenašli sa žiadne súvisiace súbory" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ni najdenih povezanih datotek" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İlgili dosya bulunamadı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Не знайдено пов'язаних файлів" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không tìm thấy tệp liên quan nào" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "未找到相关文件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "找不到相關檔案" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "找不到相關檔案" } } } }, "No Release Date" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Kein Veröffentlichungsdatum" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Sin fecha de lanzamiento" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Pas de date de sortie" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak ada tanggal rilis" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nessuna data di rilascio" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "リリース日なし" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "출시일 없음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Brak daty wydania" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Sem data de lançamento" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Sem data de lançamento" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Нет даты выпуска" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Žiadny dátum vydania" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ni datuma izdaje" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yayın Tarihi Yok" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Немає дати випуску" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không có ngày phát hành" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "没有发布日期" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "無發行日期" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "沒有發佈日期" } } } }, "No release notes were found" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Keine Versionshinweise gefunden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No se encontraron notas de la versión" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Aucune note de version trouvée" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak ada catatan rilis yang ditemukan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Non sono state trovate note di rilascio" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "リリースノートが見つかりませんでした" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "릴리스 노트를 찾을 수 없습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nie znaleziono informacji o wydaniu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nenhuma nota de lançamento encontrada" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Não foram encontradas notas de lançamento" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Не найдено заметок о выпуске" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Neboli nájdené žiadne poznámky k vydaniu" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Nobene opombe ob izdaji niso bile najdene" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sürüm notları bulunamadı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Не знайдено приміток до випуску" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không tìm thấy ghi chú phát hành" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "未找到发布说明" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "未找到發佈說明" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "未找到發佈說明" } } } }, "No releases to display" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Keine Version zum Anzeigen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No hay lanzamientos para mostrar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Aucune version à afficher" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak ada rilis untuk ditampilkan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nessuna versione da mostrare" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "表示するリリースはありません" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "표시할 릴리즈가 없습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Brak wersji do wyświetlenia" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum lançamento para exibir" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Sem lançamentos para mostrar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Нет выпусков для отображения" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Žiadne vydania na zobrazenie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ni izdaj za prikaz" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Görüntülenecek sürüm yok" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Нема релізів для відображення" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không có bản cập nhật nào" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "没有可显示的版本" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "沒有要顯示的版本" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "沒有要顯示的版本" } } } }, "No results" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Keine Ergebnisse" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Sin resultados" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Aucun résultat" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak ada hasil" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nessun risultato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "結果がありません" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "결과 없음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Brak wyników" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum resultado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Sem resultados" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Нет результатов" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Žiadne výsledky" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ni rezultatov" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sonuç yok" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Немає результатів" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không có kết quả" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "没有结果" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "沒有結果" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "沒有結果" } } } }, "No results found" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Keine Ergebnisse gefunden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No se encontraron resultados" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Aucun résultat trouvé" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak ada hasil ditemukan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nessun risultato trovato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "結果が見つかりません" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "결과를 찾을 수 없습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nie znaleziono wyników" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum resultado encontrado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum resultado encontrado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Результаты не найдены" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nenašli sa žiadne výsledky" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ni najdenih rezultatov" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sonuç bulunamadı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Результатів не знайдено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không tìm thấy kết quả nào" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "未找到结果" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "找不到結果" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "找不到結果" } } } }, "No savings available from bundle thinning" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Keine Einsparungen durch Bündelreduzierung verfügbar" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No hay ahorros disponibles por la reducción del paquete" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Aucune économie disponible grâce à l'amincissement du bundle" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak ada penghematan yang tersedia dari pengurangan bundel" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nessun risparmio disponibile dal diradamento del pacchetto" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "バンドルの削減による節約はありません" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "번들 축소로 줄일수 수 없음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Brak oszczędności wynikających z zmniejszenia rozmiaru pakietu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nenhuma economia disponível com a redução do pacote" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Nenhuma economia disponível com a redução do pacote" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Нет экономии от уменьшения пакета" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nie sú k dispozícii žiadne úspory z tenčenia balíka" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ni prihrankov pri zmanjšanju paketa" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Paket inceltmesinden tasarruf yok" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Немає заощаджень від зменшення пакета" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không có tiết kiệm nào từ việc giảm gói" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "捆绑瘦身无可用节省" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "無法從套件精簡中獲得節省" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "無法從套件瘦身中獲得儲存空間" } } } }, "No schedules configured" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Keine Zeitpläne konfiguriert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No hay horarios configurados" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Aucun horaire configuré" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak ada jadwal yang dikonfigurasi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nessun programma configurato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "スケジュールが設定されていません" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "일정이 구성되지 않음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Brak skonfigurowanych harmonogramów" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum cronograma configurado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Nenhum horário configurado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Нет настроенных расписаний" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nie sú nakonfigurované žiadne plány" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ni konfiguriranih urnikov" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hiçbir program yapılandırılmadı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Немає налаштованих розкладів" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không có lịch trình nào được cấu hình" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "未配置计划" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "未配置計劃" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "未配置時間表" } } } }, "No taps found" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Keine Wasserhähne gefunden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No se encontraron grifos" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Aucun robinet trouvé" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak ada keran ditemukan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nessun tap trovato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "タップが見つかりません" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "탭을 찾을 수 없습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nie znaleziono kranów" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nenhuma torneira encontrada" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Nenhuma torneira encontrada" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Краны не найдены" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Žiadne kohútiky nenájdené" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ni najdenih dotikov" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Musluk bulunamadı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Крани не знайдено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không tìm thấy vòi" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "找不到水龙头" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "找不到水龍頭" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "找不到水龍頭" } } } }, "No translations found" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Keine Übersetzungen gefunden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No se encontraron traducciones" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Aucune traduction trouvée" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Terjemahan tidak ditemukan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nessuna traduzione trovata" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "翻訳が見つかりません" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "번역을 찾을 수 없음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nie znaleziono tłumaczeń" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nenhuma tradução encontrada" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Nenhuma tradução encontrada" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Переводы не найдены" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nenašli sa žiadne preklady" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prevodi niso bili najdeni" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Çeviri bulunamadı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Переклади не знайдено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không tìm thấy bản dịch nào" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "未找到翻译" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "找不到翻譯" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "找不到翻譯" } } } }, "No Update 😌" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "kein Update 😌" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Sin actualización 😌" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Pas de mise à jour disponible ! 😌" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak Ada Pembaruan 😌" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nessun aggiornamento 😌" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アップデートはありません 😌" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "업데이트 없음 😌" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Brak aktualizacji 😌" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nenhuma atualização 😌" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Sem Atualização 😌" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Нет обновлений 😌" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Žiadna aktualizácia 😌" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ni posodobitev 😌" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Güncelleme Yok 😌" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Нема оновлення 😌" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không có bản cập nhật mới 😌" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "没有更新 😌" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "沒有更新 😌" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "沒有更新 😌" } } } }, "Not available" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Nicht verfügbar" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No disponible" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Non disponible" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak tersedia" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Non disponibile" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "利用不可" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "사용할 수 없음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Niedostępne" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Não disponível" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Não disponível" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Недоступно" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nie je k dispozícii" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ni na voljo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Mevcut değil" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Недоступно" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không có sẵn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "未能获取" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "無法使用" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "無法使用" } } } }, "Not Found" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Nicht gefunden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No encontrado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Non trouvé" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak Ditemukan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Non trovato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "見つかりません" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "찾을 수 없음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nie znaleziono" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Não Encontrado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Não Encontrado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Не найдено" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nenašlo sa" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ni najdeno" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bulunamadı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Не знайдено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không tìm thấy" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "未找到" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "找不到" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "找不到" } } } }, "Notes" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Notizen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Notas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Notes" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Catatan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Note" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "メモ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "노트" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Notatki" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Notas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Notas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Заметки" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Poznámky" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Opombe" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Notlar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Нотатки" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ghi chú" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "笔记" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "筆記" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "備註" } } } }, "Nuclear Reset" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Nuklearer Reset" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Reinicio Nuclear" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Réinitialisation Nucléaire" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Reset Nuklir" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ripristino Nucleare" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "核リセット" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "핵 재설정" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Reset Nuklearny" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Reinicialização Nuclear" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Reinicialização Nuclear" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Ядерный Сброс" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nukleárny Reset" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Jedrski Ponastavitev" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Nükleer Sıfırlama" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ядерне Скидання" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thiết Lập Lại Hạt Nhân" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "核重置" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "核重置" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "核重置" } } } }, "of" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "von" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "de" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "sur" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "dari" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "di" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "の" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "의" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "z" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "de" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "de" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "из" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "z" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "od" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "öğe" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "з" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "trên" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "/" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "/" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "/" } } } }, "Official" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Offiziell" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Oficial" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Officiel" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Resmi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ufficiale" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "公式" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "공식" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Oficjalny" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Oficial" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Oficial" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Официальный" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Oficiálny" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Uradno" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Resmi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Офіційний" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chính thức" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "官方" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "官方" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "官方" } } } }, "Okay" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Okay" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Aceptar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "D'accord" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Oke" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ok" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "オーケー" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "확인" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Okej" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ok" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ok" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Ок" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "OK" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "V redu" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Tamam" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Окей" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đồng ý" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "好的" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "好" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "好" } } } }, "Only show apps with savings of 1MB+" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Nur Apps mit einer Ersparnis von über 1 MB anzeigen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar solo aplicaciones con ahorro de 1 MB+" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Afficher uniquement les applications avec gain de 1 Mo ou plus" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hanya tampilkan aplikasi dengan penghematan 1MB+" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Mostra solo le app con risparmi di oltre 1MB" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "1MB以上の節約ができるアプリのみ表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "1MB 이상의 절약이 있는 앱만 표시" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pokaż tylko aplikacje, które pozwalają zaoszczędzić co najmniej 1 MB" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar apenas aplicativos com economia de 1MB+" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar apenas aplicativos com economia de 1MB+" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Показывать только приложения с экономией от 1 МБ+" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobrazovať iba aplikácie s úsporou 1MB+" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prikaži samo aplikacije s prihrankom 1 MB+" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yalnızca 1MB+ tasarruf sağlayan uygulamaları göster" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Показувати лише програми з економією 1MB+" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chỉ hiển thị ứng dụng có tiết kiệm từ 1MB trở lên" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "仅显示可节省 1MB 以上的应用程序" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "只顯示節省超過 1MB 的應用程式" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "僅顯示節省超過 1MB 的應用程式" } } } }, "Open" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Öffnen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Abrir" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ouvrir" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Buka" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Apri" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "開く" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "열기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Otwórz" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Abrir" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Abrir" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Открыть" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Otvoriť" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odpri" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Aç" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Відкрити" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Mở" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "打开" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "打開" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "開啟" } } } }, "Open %@" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Öffne %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Abrir %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ouvrir %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Buka %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Apri %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%@を開く" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@ 열기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Otwórz %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Abrir %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Abrir %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Открыть %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Otvoriť %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odpri %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%@’ı aç" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Відкрити %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Mở %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "打开 %@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "開啟 %@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "開啟 %@" } } } }, "Open in Finder" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Im Finder öffnen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Abrir en Finder" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ouvrir le Finder" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Buka di Finder" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Apri nel Finder" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Finderで開く" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Finder에서 열기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Otwórz w Finder" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Abrir no Finder" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Abrir no Finder" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Открыть в Finder" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Otvoriť vo Finderi" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odpri v Finderju" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Finder'da Aç" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Відкрити у Finder" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Mở trong Finder" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "在访达中打开" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "在 Finder 中開啟" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "在 Finder 中開啟" } } } }, "Open Settings to enable the Privileged Helper" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Öffnen Sie die Einstellungen, um den privilegierten Helfer zu aktivieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Abra Configuración para habilitar el Asistente Privilegiado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ouvrez les Paramètres pour activer l'Assistant Privilégié" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Buka Pengaturan untuk mengaktifkan Pembantu Istimewa" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Apri Impostazioni per abilitare l'Assistente Privilegiato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "特権ヘルパーを有効にするには設定を開いてください" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "권한 상승 도우미를 활성화하려면 설정을 엽니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Otwórz Ustawienia, aby włączyć Pomocnika z uprawnieniami" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Abra as Configurações para ativar o Assistente Privilegiado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Abra as Definições para ativar o Assistente Privilegiado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Откройте настройки, чтобы включить привилегированного помощника" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Otvorte Nastavenia, aby ste povolili Privilegovaného Pomocníka" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odprite nastavitve, da omogočite privilegiranega pomočnika" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ayrıcalıklı Yardımcıyı etkinleştirmek için Ayarları açın" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Відкрийте Налаштування, щоб увімкнути Привілейованого Помічника" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Mở Cài đặt để bật Trợ lý Đặc quyền" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "打开设置以启用特权助手" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "開啟設定以啟用特權助手" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "開啟設定以啟用特權助手" } } } }, "Open System Settings" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Systemeinstellungen öffnen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Abrir configuración del sistema" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ouvrir les paramètres système" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Buka Pengaturan Sistem" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Apri Impostazioni di Sistema" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "システム設定を開く" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "시스템 설정 열기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Otwórz ustawienia systemowe" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Abrir configurações do sistema" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Abrir Definições do Sistema" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Открыть системные настройки" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Otvoriť nastavenia systému" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odpri sistemske nastavitve" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sistem Ayarlarını Aç" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Відкрити налаштування системи" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Mở cài đặt hệ thống" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "打开系统设置" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "開啟系統設定" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "開啟系統設定" } } } }, "Open System Settings to grant permissions" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Öffnen Sie die Systemeinstellungen, um Berechtigungen zu erteilen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Abre la configuración del sistema para otorgar permisos" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ouvrez les paramètres système pour accorder les autorisations" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Buka Pengaturan Sistem untuk memberikan izin" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Apri le Impostazioni di Sistema per concedere le autorizzazioni" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "システム設定を開いて権限を付与してください" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "권한을 부여하려면 시스템 설정을 엽니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Otwórz ustawienia systemowe, aby przyznać uprawnienia" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Abra as Configurações do Sistema para conceder permissões" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Abrir Definições do Sistema para conceder permissões" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Откройте настройки системы, чтобы предоставить разрешения" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Otvorte systémové nastavenia na udelenie povolení" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odprite sistemske nastavitve za dodelitev dovoljenj" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İzin vermek için Sistem Ayarlarını açın" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Відкрийте налаштування системи, щоб надати дозволи" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Mở Cài đặt Hệ thống để cấp quyền" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "打开系统设置以授予权限" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "開啟系統設定以授予權限" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "開啟系統設定以授予權限" } } } }, "Operation failed: %@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Vorgang fehlgeschlagen: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Operación fallida: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Echec de l’opération: %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Operasi gagal: %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Operazione fallita: %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "操作に失敗しました: %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "작업 실패: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Operacja nie powiodła się: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Falha na operação: %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Operação falhou: %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Операция не удалась: %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Operácia zlyhala: %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Operacija ni uspela: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İşlem başarısız oldu: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Операція не вдалася: %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thao tác thất bại: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "操作失败: %@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "操作失敗:%@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "操作失敗:%@" } } } }, "Optional Dependencies" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Optionale Abhängigkeiten" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Dependencias opcionales" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Dépendances optionnelles" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Dependensi Opsional" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Dipendenze opzionali" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "オプションの依存関係" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "선택적 종속성" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Opcjonalne zależności" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Dependências Opcionais" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Dependências Opcionais" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Необязательные зависимости" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Voliteľné závislosti" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Neobvezne odvisnosti" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Opsiyonel Bağımlılıklar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Необов'язкові залежності" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Phụ thuộc tùy chọn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "可选依赖" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "可選依賴" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "可選依賴" } } } }, "Options" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Optionen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Opciones" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Options" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Opsi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Opzioni" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "オプション" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "옵션" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Opcje" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Opções" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Opções" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Настройки" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Možnosti" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Možnosti" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Seçenekler" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Параметри" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tùy chọn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "选项" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "選項" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "選項" } } } }, "Orphaned file search is not 100% accurate as it doesn't have any uninstalled app bundles to check against for file exclusion. This does a best guess search for files/folders and excludes the ones that have overlap with your currently installed applications. Please confirm files marked for deletion really do belong to uninstalled applications." : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Die Suche nach verwaisten Dateien ist nicht zu 100% genau, da sie keine deinstallierten Anwendungspakete findet, welche für den Dateiausschluss überprüfen werden können. Die Suche nach Dateien/Ordnern erfolgt nach bestem Wissen und Gewissen und schliesst diejenigen aus, die sich mit den derzeit installierten Anwendungen überschneiden. Bitte vergewissern Sie sich, dass die zum Löschen markierten Dateien wirklich zu deinstallierten Anwendungen gehören." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "La búsqueda de archivos huérfanos no es 100% precisa ya que no tiene ningún paquete de aplicaciones desinstalado con el que verificar la exclusión de archivos. Realiza una búsqueda aproximada de archivos/carpetas y excluye aquellos que se superponen con tus aplicaciones actualmente instaladas. Por favor confirma que los archivos marcados para eliminación realmente pertenecen a aplicaciones desinstaladas." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "La recherche de fichiers résiduels n'est pas 100 % précise car elle ne dispose d'aucune information concernant les applications anciennement désinstallées pour faciliter l’exclusion de certains fichiers. Ceci effectue donc une recherche approximative des fichiers et dossiers et exclut ceux qui sont similaires à vos applications actuellement installées. Veuillez vérifier avec attention que les fichiers marqués pour suppression appartiennent bien à des applications désinstallées." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pencarian file yatim tidak 100% akurat karena tidak memiliki bundel aplikasi yang dihapus untuk diperiksa dalam pengecualian file. Hal ini akan melakukan pencarian dengan perkiraan terbaik untuk file/folder dan mengecualikan file/folder yang tumpang tindih dengan aplikasi yang saat ini terpasang. Harap konfirmasi bahwa file yang ditandai untuk dihapus benar-benar milik aplikasi yang dihapus." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "La ricerca di file orfani non è accurata al 100% poiché non dispone di pacchetti di app disinstallate da confrontare per l'esclusione dei file. Questo effettua una ricerca basata sulla migliore ipotesi per file/cartelle ed esclude quelli che si sovrappongono alle applicazioni attualmente installate. Si prega di confermare che i file contrassegnati per l'eliminazione appartengano effettivamente ad applicazioni disinstallate." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "孤立ファイル検索は100%正確ではありません。アンインストールされたアプリのバンドルがないため、ファイルの除外に対するチェックができません。これはファイル/フォルダーのベストガイス検索を行い、現在インストールされているアプリケーションと重複するものを除外します。削除対象としてマークされたファイルが本当にアンインストールされたアプリケーションに属していることを確認してください。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "연결되지 않은 파일 검색은 파일 제외를 위한 제거된 앱 번들이 없기 때문에 100% 정확하지 않습니다. 이 검색은 파일/폴더에 대한 최상의 추측된 검색을 하며, 현재 설치된 애플리케이션과 겹치는 파일을 제외합니다. 삭제로 표시된 파일이 실제로 제거된 애플리케이션에 속하는지 확인해 주세요." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyszukiwanie osieroconych plików nie jest w 100% dokładne, ponieważ nie ma żadnych odinstalowanych pakietów aplikacji, które można by sprawdzić pod kątem wykluczenia plików. Funkcja ta wykonuje najlepsze możliwe wyszukiwanie plików/folderów i wyklucza te, które pokrywają się z aktualnie zainstalowanymi aplikacjami. Proszę potwierdzić, że pliki oznaczone do usunięcia rzeczywiście należą do odinstalowanych aplikacji." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "A busca por arquivos órfãos não é 100% precisa, pois não há conjuntos de aplicativos desinstalados para verificar a exclusão de arquivos. Isso faz uma busca de palpite para arquivos/pastas e exclui aqueles que têm sobreposição com suas aplicações atualmente instaladas. Por favor, confirme se os arquivos marcados para exclusão realmente pertencem a aplicativos desinstalados." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A pesquisa de ficheiros órfãos não é 100% precisa, pois não tem pacotes de apps desinstaladas para verificar a exclusão de ficheiros. Faz uma pesquisa de melhor suposição para ficheiros/pastas e exclui aqueles que têm sobreposição com as suas aplicações atualmente instaladas. Por favor, confirme se os ficheiros marcados para eliminação realmente pertencem a aplicações desinstaladas." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Поиск потерянных файлов не является точным на 100%, поскольку в нем нет пакетов удаленных приложений, которые можно было бы проверить на предмет исключения файлов. Поиск файлов/папок выполняется на основе наилучшего предположения и исключает те, которые пересекаются с вашими текущими установленными приложениями. Пожалуйста, подтвердите, что файлы, помеченные для удаления, действительно принадлежат удаленным приложениям." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vyhľadávanie osamotených súborov nie je 100% presné, pretože neobsahuje žiadne balíky odinštalovaných aplikácií, ktoré by mohol použiť na vylúčenie súborov. Toto je len odhadované vyhľadávanie súborov/priečinkov a vylučuje tie, ktoré sa prekrývajú s aktuálne nainštalovanými aplikáciami. Prosím potvrď, že súbory označené na odstránenie naozaj patria k odinštalovaným aplikáciám." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Iskanje osirotelih datotek ni 100% natančno, saj nima nobenih odstranjenih paketov aplikacij za preverjanje izključitve datotek. To je najboljša ugibana iskanje datotek/map in izključuje tiste, ki se prekrivajo z vašimi trenutno nameščenimi aplikacijami. Prosimo, potrdite, da datoteke, označene za brisanje, resnično pripadajo odstranjenim aplikacijam." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Başıboş dosya araması, dosya hariç tutma için kontrol edilecek kaldırılmış uygulama paketleri olmadığından %100 doğru değildir. Bu, dosyalar/klasörler için en iyi tahminle bir arama yapar ve halihazırda yüklü olan uygulamalarınızla çakışanları hariç tutar. Lütfen silinmek üzere işaretlenen dosyaların gerçekten kaldırılmış uygulamalara ait olduğunu doğrulayın." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Пошук осиротілих файлів не є на 100% точним, оскільки він не має пакунків видалених програм, які можна було б перевірити на предмет виключення файлів. Виконується пошук файлів/папок на основі найкращих припущень і виключаються ті, що перетинаються з вашими поточними встановленими програмами. Будь ласка, переконайтеся, що файли, позначені для видалення, дійсно належать до видалених програм." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Việc tìm kiếm tệp rác không hoàn toàn chính xác vì không có các gói ứng dụng đã gỡ cài đặt để kiểm tra và loại trừ các tệp. Quá trình này thực hiện tìm kiếm ước lượng tốt nhất cho các tệp/thư mục và loại trừ những tệp có sự trùng lặp với các ứng dụng hiện tại. Vui lòng xác nhận các tệp được đánh dấu thuộc các ứng dụng đã gỡ cài đặt." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "孤立文件搜索并不 100% 准确,因为没有可供检查的已卸载应用程序包以进行文件排除。这是对文件/文件夹的最佳猜测搜索,并排除了与您当前安装的应用程序重叠的文件。请确认标记为删除的文件确实属于已卸载的应用程序。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "「孤立檔案搜尋」並非 100% 準確,因為它沒有任何解除安裝的應用程式套裝來檢查檔案排除。這會以最佳猜測搜尋檔案/資料夾,並排除與你現時安裝的應用程式重疊的檔案/資料夾。請確認標記為移除的檔案確實屬於已解除安裝的應用程式。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "「孤立檔案搜尋」並非 100% 準確,因為它沒有任何解除安裝的應用程式套裝來檢查檔案排除。這會以最佳猜測搜尋檔案/資料夾,並排除與你現時安裝的應用程式重疊的檔案/資料夾。請確認標記為移除的檔案確實屬於已解除安裝的應用程式。" } } } }, "Orphaned Files" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Verwaiste Dateien" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Archivos Huérfanos" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Fichiers résiduels" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "File Yatim" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "File orfani" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "孤立ファイル" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "연결되지 않은 파일" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pliki osierocone" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Arquivos Órfãos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ficheiros Órfãos" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Остаточные файлы" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Osamotené súbory" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Osirotele datoteke" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Başıboş Dosyalar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Осиротілі Файли" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tệp rác" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "孤立文件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "孤立檔案" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "孤立檔案" } } } }, "Orphans" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Verwaiste Dateien" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Huérfanas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Orphelin(e)s" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Yatim" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Orfani" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "孤立ファイル" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "연결 안됨" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pliki osierocone" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Órfãos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Órfãos" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Остаточные файлы" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Siroty" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Osirotki" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yetimler" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Залишкові файли" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tập tin mồ côi" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "孤立文件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "孤立檔案" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "孤立文件" } } } }, "Outdated" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Veraltet" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Desactualizado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Obsolète" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Kedaluwarsa" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Obsoleto" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "時代遅れ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "구식" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Przestarzały" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Desatualizado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Desatualizado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Устаревший" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zastaraný" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Zastarelo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Güncel değil" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Застарілий" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Lỗi thời" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "过时" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "過時" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "過時" } } } }, "Package Manager" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Paketmanager" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Administrador de Paquetes" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Gestionnaire de paquets" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pengelola Paket" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Gestore pacchetti" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "パッケージマネージャー" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "패키지 관리자" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Menedżer pakietów" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Gerenciador de Pacotes" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Gestor de Pacotes" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Менеджер пакетов" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Správca balíkov" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Upravitelj paketov" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Paket Yöneticisi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Диспетчер пакетів" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Trình quản lý gói" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "包管理器" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "套件管理器" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "套件管理器" } } } }, "Packages" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Pakete" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Paquetes" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Paquets" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Paket" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Pacchetti" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "パッケージ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "패키지" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pakiety" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Pacotes" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Pacotes" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Пакеты" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Balíky" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Paketi" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Paketler" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Пакунки" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Gói cài đặt" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "软件包" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "軟體包" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "套裝" } } } }, "Password cache timeout" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Zeitüberschreitung des Passwort-Caches" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Tiempo de espera de la caché de contraseña" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Délai d'expiration du cache de mot de passe" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Waktu habis cache kata sandi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Timeout della cache della password" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "パスワードキャッシュのタイムアウト" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "비밀번호 캐시 시간 초과" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Limit czasu pamięci podręcznej hasła" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Tempo limite do cache de senha" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Tempo de expiração da cache de senha" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Тайм-аут кэша пароля" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Časový limit vyrovnávacej pamäte hesla" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Časovna omejitev predpomnilnika gesla" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Parola önbellek zaman aşımı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Тайм-аут кешу пароля" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thời gian chờ bộ nhớ đệm mật khẩu" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "密码缓存超时" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "密碼快取逾時" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "密碼快取逾時" } } } }, "Path" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Pfad" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ruta" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Chemin d’accès" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Jalur" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Percorso" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "パス" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "경로" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Scieżka" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Caminho" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Caminho" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Путь" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Cesta" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pot" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Dosya Yolu" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Шлях" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đường dẫn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "路径" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "路徑" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "路徑" } } } }, "Path / Keyword" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Pfad / Schlüsselwort" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ruta / Palabra clave" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Chemin / Mot-clé" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Jalur / Kata Kunci" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Percorso / Parola chiave" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "パス / キーワード" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "경로 / 키워드" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ścieżka / Słowo kluczowe" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Caminho / Palavra-chave" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Caminho / Palavra-chave" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Путь / Ключевое слово" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Cesta / Kľúčové slovo" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pot / Ključna beseda" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yol / Anahtar Kelime" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Шлях / Ключове слово" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đường dẫn / Từ khóa" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "路径 / 关键词" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "路徑 / 關鍵字" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "路徑 / 關鍵字" } } } }, "Path: %@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Pfad: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ruta: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Chemin : %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Jalur: %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Percorso: %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "パス: %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "경로: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ścieżka: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Caminho: %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Caminho: %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Путь: %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Cesta: %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pot: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yol: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Шлях: %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đường dẫn: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "路径:%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "路徑:%@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "路徑:%@" } } } }, "Pearcleaner and all of its files will be cleanly removed, are you sure?" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner und alle seine Dateien werden sauber entfernt, sind Sie sicher?" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "¿Está seguro de que desea eliminar Pearcleaner y todos sus archivos?" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner et tous ses fichiers seront supprimés, êtes-vous sûr(e) ?" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner dan semua filenya akan dihapus dengan bersih, apakah Anda yakin?" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner e tutti i suoi file verranno rimossi completamente, sei sicuro?" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner とそのすべてのファイルが完全に削除されます。よろしいですか?" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner와 그 모든 파일이 깨끗하게 제거됩니다. 확실합니까?" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner i wszystkie jego pliki zostaną całkowicie usunięte. Czy na pewno chcesz to zrobić?" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "O Pearcleaner e todos os seus arquivos serão removidos de forma limpa, tem certeza?" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "O Pearcleaner e todos os seus ficheiros serão removidos de forma limpa, tem a certeza?" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner и все его файлы будут полностью удалены, вы уверены?" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner a všetky jeho súbory budú úplne odstránené, si si istý?" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner in vse njegove datoteke bodo čisto odstranjene, ste prepričani?" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner ve onun tüm dosyaları tamamen kaldırılacak, emin misiniz?" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner і всі його файли буде повністю видалено, ви впевнені?" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner và tất cả các tệp của nó sẽ bị gỡ bỏ sạch sẽ, bạn có chắc chắn không?" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner 及其所有文件将被干净地移除,您确定吗?" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner 及其全部檔案將會完全移除,你確定嗎?" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner 及其全部檔案將會完全移除,你確定嗎?" } } } }, "Pearcleaner CLI support" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner CLI Unterstützung" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Soporte para Pearcleaner CLI" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ajouter Pearcleaner à l’interface de ligne de commande" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Dukungan CLI/Baris Perintah Pearcleaner" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Supporto CLI Pearcleaner" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner CLI サポート" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner CLI 지원" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Obsługa Pearcleaner CLI" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Suporte CLI do Pearcleaner" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Suporte CLI do Pearcleaner" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Поддержка CLI Pearcleaner" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner CLI podpora" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Podpora za Pearcleaner CLI" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner CLI desteği" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Підтримка Pearcleaner CLI" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hỗ trợ sử dụng Pearcleaner bằng dòng lệnh" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner CLI 支持" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner 命令列介面(CLI)支援" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner 命令列介面(CLI)支援" } } } }, "Pearcleaner requires permissions to search all system locations comprehensively." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner benötigt Berechtigungen, um alle Systemstandorte umfassend zu durchsuchen." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner requiere permisos para buscar en todas las ubicaciones del sistema de manera exhaustiva." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner nécessite des autorisations pour rechercher de manière exhaustive tous les emplacements système." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner memerlukan izin untuk mencari semua lokasi sistem secara menyeluruh." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner richiede autorizzazioni per cercare in modo completo tutte le posizioni di sistema." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner はすべてのシステムの場所を包括的に検索するための権限が必要です。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner는 모든 시스템 위치를 포괄적으로 검색하기 위해 권한이 필요합니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner wymaga uprawnień do kompleksowego przeszukiwania wszystkich lokalizacji systemowych." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner requer permissões para pesquisar de forma abrangente todos os locais do sistema." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner requer permissões para pesquisar todos os locais do sistema de forma abrangente." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner требует разрешений для всестороннего поиска по всем системным местоположениям." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner vyžaduje povolenia na komplexné prehľadávanie všetkých systémových umiestnení." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner potrebuje dovoljenja za celovito iskanje po vseh sistemskih lokacijah." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner, tüm sistem konumlarını kapsamlı bir şekilde aramak için izin gerektirir." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner потребує дозволів для всебічного пошуку у всіх системних місцях." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner yêu cầu quyền để tìm kiếm toàn diện tất cả các vị trí hệ thống." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner 需要权限来全面搜索所有系统位置。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner 需要權限來全面搜索所有系統位置。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner 需要權限來全面搜尋所有系統位置。" } } } }, "Pearcleaner Uninstall" : { "comment" : "Right click context menu item when using the Finder extension to uninstall using Pearcleaner", "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner Deinstallation" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalar Pearcleaner" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Désinstaller avec Pearcleaner" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Copot Pearcleaner" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Disinstalla Pearcleaner" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleanerでアンインストール" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner 제거" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Odinstaluj za pomocą Pearcleaner" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalar o Pearcleaner" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalar Pearcleaner" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить через Pearcleaner" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odinštalovať Pearcleanerom" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odstrani Pearcleaner" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner ile Kaldırma" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити через Pearcleaner" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Gỡ cài đặt Pearcleaner" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner 卸载" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner 解除安裝" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner 解除安裝" } } } }, "Pearcleaner will ask you to enter your password once to enable the helper, then all subsequent privileged operations will run without any other prompts as long as the helper stays enabled in Settings > Login Items. This authorization is all managed by macOS via SMAppService." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner wird Sie einmal nach Ihrem Passwort fragen, um den Helfer zu aktivieren, dann werden alle nachfolgenden privilegierten Operationen ohne weitere Aufforderungen ausgeführt, solange der Helfer in Einstellungen > Anmeldeobjekte aktiviert bleibt. Diese Autorisierung wird von macOS über SMAppService verwaltet." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner te pedirá que ingreses tu contraseña una vez para habilitar el asistente, luego todas las operaciones privilegiadas posteriores se ejecutarán sin ningún otro aviso mientras el asistente permanezca habilitado en Configuración > Elementos de inicio de sesión. Esta autorización es gestionada por macOS a través de SMAppService." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner vous demandera de saisir votre mot de passe une fois pour activer l'assistant, puis toutes les opérations privilégiées suivantes s'exécuteront sans aucune autre invite tant que l'assistant reste activé dans Réglages > Éléments de connexion. Cette autorisation est gérée par macOS via SMAppService." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner akan meminta Anda memasukkan kata sandi sekali untuk mengaktifkan pembantu, kemudian semua operasi istimewa berikutnya akan berjalan tanpa permintaan lain selama pembantu tetap diaktifkan di Pengaturan > Item Masuk. Otorisasi ini dikelola oleh macOS melalui SMAppService." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner ti chiederà di inserire la tua password una volta per abilitare l'assistente, quindi tutte le operazioni privilegiate successive verranno eseguite senza altri avvisi finché l'assistente rimane abilitato in Impostazioni > Elementi di accesso. Questa autorizzazione è gestita da macOS tramite SMAppService." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner は、ヘルパーを有効にするために一度だけパスワードを入力するよう求め、その後、ヘルパーが設定 > ログイン項目で有効なままである限り、すべての後続の特権操作は他のプロンプトなしで実行されます。この承認は macOS によって SMAppService 経由で管理されます。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner는 도우미를 활성화하기 위해 한 번 비밀번호를 입력하도록 요청한 후, 도우미가 설정 > 로그인 항목에서 활성화된 상태로 유지되는 한 모든 후속 권한 작업은 다른 프롬프트 없이 실행됩니다. 이 권한 부여는 SMAppService를 통해 macOS에서 관리됩니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner poprosi Cię o podanie hasła raz, aby włączyć pomocnika, a następnie wszystkie kolejne operacje uprzywilejowane będą działać bez żadnych innych monitów, dopóki pomocnik pozostaje włączony w Ustawieniach > Elementy logowania. Ta autoryzacja jest zarządzana przez macOS za pośrednictwem SMAppService." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "O Pearcleaner pedirá que você insira sua senha uma vez para ativar o assistente, então todas as operações privilegiadas subsequentes serão executadas sem qualquer outro aviso, desde que o assistente permaneça ativado em Configurações > Itens de Login. Esta autorização é gerenciada pelo macOS via SMAppService." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "O Pearcleaner pedirá que você insira sua senha uma vez para habilitar o assistente, então todas as operações privilegiadas subsequentes serão executadas sem outros avisos, desde que o assistente permaneça habilitado em Ajustes > Itens de Início de Sessão. Esta autorização é toda gerida pelo macOS via SMAppService." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner попросит вас ввести пароль один раз, чтобы включить помощник, после чего все последующие привилегированные операции будут выполняться без дополнительных запросов, пока помощник остается включенным в Настройки > Элементы входа. Это разрешение управляется macOS через SMAppService." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner vás požiada o zadanie hesla raz na povolenie pomocníka, potom všetky nasledujúce privilegované operácie budú prebiehať bez ďalších výziev, pokiaľ pomocník zostane povolený v Nastaveniach > Položky prihlásenia. Toto overenie je spravované systémom macOS prostredníctvom SMAppService." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner vas bo prosil, da enkrat vnesete geslo za omogočanje pomočnika, nato pa bodo vse nadaljnje privilegirane operacije potekale brez drugih pozivov, dokler bo pomočnik ostal omogočen v Nastavitve > Elementi za prijavo. To avtorizacijo upravlja macOS prek SMAppService." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner, yardımcıyı etkinleştirmek için sizden bir kez şifrenizi girmenizi isteyecek, ardından yardımcı Ayarlar > Giriş Öğeleri'nde etkin kaldığı sürece tüm sonraki ayrıcalıklı işlemler başka bir uyarı olmadan çalışacaktır. Bu yetkilendirme, macOS tarafından SMAppService aracılığıyla yönetilir." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner попросить вас ввести пароль один раз, щоб увімкнути помічника, після чого всі наступні привілейовані операції будуть виконуватися без будь-яких інших запитів, доки помічник залишається увімкненим у Налаштуваннях > Елементи входу. Ця авторизація керується macOS через SMAppService." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner sẽ yêu cầu bạn nhập mật khẩu một lần để kích hoạt trình trợ giúp, sau đó tất cả các thao tác đặc quyền tiếp theo sẽ chạy mà không có bất kỳ lời nhắc nào khác miễn là trình trợ giúp vẫn được bật trong Cài đặt > Mục đăng nhập. Sự ủy quyền này được quản lý bởi macOS thông qua SMAppService." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner 会要求您输入密码一次以启用助手,然后所有后续的特权操作将在没有其他提示的情况下运行,只要助手在设置 > 登录项中保持启用状态。此授权由 macOS 通过 SMAppService 管理。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner 會要求您輸入密碼一次以啟用助手,然後所有後續的特權操作將在不提示的情況下運行,只要助手在設定 > 登入項目中保持啟用狀態。此授權由 macOS 通過 SMAppService 管理。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner 會要求您輸入密碼一次以啟用助手,然後所有後續的特權操作將在不提示的情況下運行,只要助手在設定 > 登入項目中保持啟用狀態。此授權由 macOS 通過 SMAppService 管理。" } } } }, "Pearcleaner will strip the %@ architecture from %@'s executable file to save space. Would you like to proceed?" : { "comment" : "Lipo alert message", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner wird die %@-Architektur aus der ausführbaren Datei von %@ entfernen, um Speicherplatz zu sparen. Möchten Sie fortfahren?" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner eliminará la arquitectura %@ del archivo ejecutable de %@ para ahorrar espacio. ¿Desea continuar?" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner supprimera l'architecture %@ du fichier exécutable de %@ pour économiser de l'espace. Voulez-vous continuer ?" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner akan menghapus arsitektur %@ dari file eksekusi %@ untuk menghemat ruang. Apakah Anda ingin melanjutkan?" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner rimuoverà l'architettura %@ dal file eseguibile di %@ per risparmiare spazio. Vuoi procedere?" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleanerは、スペースを節約するために%@の実行可能ファイルから%@アーキテクチャを削除します。続行しますか?" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner가 공간을 절약하기 위해 %@의 실행 파일에서 %@ 아키텍처를 제거합니다. 계속하시겠습니까?" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner usunie architekturę %@ z pliku wykonywalnego %@, aby zaoszczędzić miejsce. Czy chcesz kontynuować?" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner removerá a arquitetura %@ do arquivo executável de %@ para economizar espaço. Deseja continuar?" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "O Pearcleaner irá remover a arquitetura %@ do arquivo executável de %@ para economizar espaço. Deseja continuar?" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner удалит архитектуру %@ из исполняемого файла %@ для экономии места. Хотите продолжить?" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner odstráni architektúru %@ z vykonávacieho súboru %@, aby ušetril miesto. Chcete pokračovať?" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner bo odstranil arhitekturo %@ iz izvedljive datoteke %@, da prihrani prostor. Želite nadaljevati?" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner, alan kazanmak için %@'nin çalıştırılabilir dosyasından %@ mimarisini kaldıracak. Devam etmek istiyor musunuz?" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner видалить архітектуру %@ з виконуваного файлу %@, щоб заощадити місце. Бажаєте продовжити?" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner sẽ loại bỏ kiến trúc %@ khỏi tập tin thực thi của %@ để tiết kiệm không gian. Bạn có muốn tiếp tục không?" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner将从%@的可执行文件中剥离%@架构以节省空间。您想继续吗?" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner將從%@的可執行文件中剝離%@架構以節省空間。您想繼續嗎?" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner將會從%@的可執行檔中移除%@架構以節省空間。你想繼續嗎?" } } } }, "Pending schedules" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ausstehende Zeitpläne" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Horarios pendientes" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Horaires en attente" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Jadwal tertunda" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Programmi in sospeso" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "保留中のスケジュール" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "대기 중인 일정" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Oczekujące harmonogramy" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Agendas pendentes" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Agendamentos pendentes" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Ожидающие расписания" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Čakajúce plány" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Čakajoči urniki" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bekleyen programlar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Очікувані розклади" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Lịch trình đang chờ xử lý" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "待定日程" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "待定日程" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "待定日程" } } } }, "Perform privileged operations seamlessly without password prompts" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Führt privilegierte Vorgänge nahtlos und ohne Passwortabfrage aus." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Realiza operaciones privilegiadas sin interrupciones y sin solicitudes de contraseña" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Effectuez des opérations nessécitant les privilégiées admin de manière transparente sans invite de mot de passe" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lakukan operasi istimewa tanpa hambatan tanpa permintaan kata sandi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Esegui operazioni privilegiate senza interruzioni senza richieste di password" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "パスワード入力なしでシームレスに特権操作を実行" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "비밀번호 입력 없이 원활하게 권한 있는 작업 수행" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Płynne wykonywanie operacji uprzywilejowanych bez komunikatów o podanie hasła" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Realize operações privilegiadas de forma tranquila sem solicitações de senha" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Realize operações privilegiadas sem interrupções e sem solicitações de senha" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Выполняйте привилегированные операции без запросов пароля" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vykonávajte privilégiované operácie plynulo bez výziev na zadanie hesla" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izvajajte privilegirane operacije brez pozivov za geslo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Parola istemleri olmadan ayrıcalıklı işlemleri sorunsuz bir şekilde gerçekleştirin" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Виконуйте привілейовані операції без запитів на введення пароля" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thực hiện các thao tác cần thẩm quyền mà không cần yêu cầu mật khẩu" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "无缝执行带权限的操作,无需密码提示" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "在沒有密碼的提示下流暢地執行特殊權限操作" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "在沒有密碼的提示下流暢地執行特殊權限操作" } } } }, "Perform updates on 3rd party apps, and more" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Führen Sie Updates für Drittanbieter-Apps durch und mehr" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Realizar actualizaciones en aplicaciones de terceros y más" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Effectuer des mises à jour sur des applications tierces, et plus encore" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lakukan pembaruan pada aplikasi pihak ketiga, dan lainnya" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Esegui aggiornamenti su app di terze parti e altro ancora" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "サードパーティアプリの更新を実行するなど" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "타사 앱을 업데이트하고 더 많은 작업 수행" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wykonuj aktualizacje aplikacji innych firm i nie tylko" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Realize atualizações em aplicativos de terceiros e mais" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Realizar atualizações em apps de terceiros, e mais" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Выполняйте обновления сторонних приложений и многое другое" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vykonajte aktualizácie na aplikáciách tretích strán a ďalšie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izvedite posodobitve na aplikacijah tretjih oseb in še več" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Üçüncü taraf uygulamalarda güncellemeler yapın ve daha fazlası" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Виконуйте оновлення сторонніх додатків та інше" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thực hiện cập nhật trên ứng dụng bên thứ ba và hơn thế nữa" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "执行第三方应用程序更新,等等" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "執行第三方應用程式更新,等等" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "執行第三方應用程式更新,等等" } } } }, "Permission required. Enable in System Settings > Login Items." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Berechtigung notwendig. Einschalten in den Systemeinstellungen > Anmeldeobjekte" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Permiso necesario. Habilite en Configuración del sistema > Elementos de inicio de sesión." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Autorisation requise. Activer dans Paramètres système > Éléments de connexion." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Izin diperlukan. Aktifkan di Pengaturan Sistem > Item Masuk." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Autorizzazione richiesta. Abilita in Impostazioni di Sistema > Elementi di Login." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "許可が必要です。システム設定 > ログイン項目で有効にしてください。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "권한이 필요합니다. 시스템 설정 > 로그인 항목에서 활성화하세요." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wymagane uprawnienia. Włącz w Ustawieniach systemowych > Rzeczy i rozszerzenia otwierane podczas logowania." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Permissão necessária. Ative em Ajustes do Sistema > Itens de Login." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Permissão necessária. Ative em Definições do Sistema > Itens de Início de Sessão." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Требуется разрешение. Включите в Системные настройки > Элементы входа." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Je potrebné povolenie. Povoľte v Systémových nastaveniach > Položky pri spustení." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Potrebno dovoljenje. Omogočite v Sistemske nastavitve > Prijavni elementi." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İzin gerekli. Sistem Ayarları > Oturum Açma Öğeleri'nde etkinleştirin." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Для продовження потрібен дозвіл. Увімкніть у Системних налаштуваннях > Елементи входу" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Cần quyền truy cập. Hãy bật trong Cài đặt Hệ thống > Mục Đăng nhập." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "需要权限,在系统设置 > 登录项中启用。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "需要權限。在「系統設定」>「登入項目」中啟用。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "需要權限。在「系統設定」>「登入項目」中啟用。" } } } }, "Permission Status" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Berechtigungsstatus" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Estado de permisos" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Statut des autorisations" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Status Izin" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Stato dei permessi" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "権限ステータス" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "권한 상태" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Status uprawnień" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Status de Permissão" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Estado da Permissão" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Статус прав" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Stav povolení" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Status dovoljenja" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İzin Durumu" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Статус Дозволів" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thông tin cấp quyền" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "权限状态" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "權限狀態" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "權限狀態" } } } }, "Permissions" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Berechtigung" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Permisos" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Permissions" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Izin" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Permessi" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "権限" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "권한" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Uprawnienia" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Permissões" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Permissões" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Права" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Povolenia" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Dovoljenja" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İzinler" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Дозволи" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Quyền truy cập" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "查看权限" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "權限" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "權限" } } } }, "Permissions Missing!" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Berechtigungen fehlen!" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "¡Faltan permisos!" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Autorisations manquantes !" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Izin Hilang!" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Permessi mancanti!" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "権限がありません!" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "권한 누락!" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Brak uprawnień!" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Permissões ausentes!" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Permissões em falta!" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Отсутствуют разрешения!" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Chýbajúce povolenia!" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Manjkajo dovoljenja!" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İzinler Eksik!" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Відсутні дозволи!" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thiếu quyền!" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "权限缺失!" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "權限缺失!" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "權限缺失!" } } } }, "PID: %@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Prozess-ID: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "PID: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "PID: %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "PID: %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "PID: %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "PID: %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "PID: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "PID: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "PID: %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "PID: %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "PID: %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "PID: %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "PID: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "PID: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ідентифікатор процесу: %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "PID: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "进程ID: %@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "進程識別碼:%@\n" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "PID: %@" } } } }, "Pin popover window on top" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Popover-Fenster oben anheften" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Fijar ventana emergente en la parte superior" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Empêcher la fermeture des détails d’une l’application si clic externe" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Semat jendela popover di atas" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Fissa la finestra popover in cima" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ポップオーバーウィンドウを最上部に固定" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "팝오버 창을 맨 위에 고정" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Przypnij wyskakujące okno na górze" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Fixar a janela pop-up no topo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Fixar janela popover no topo" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Закрепить всплывающее окно сверху" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pripnúť vyskakovacie okno na vrch" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pripni pojavno okno na vrh" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Açılır pencereyi en üste sabitle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Закріпіть спливаюче вікно зверху" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ghim cửa sổ nổi lên trên cùng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "将弹出窗口固定在顶部" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "固定彈出式視窗於最上方" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "固定彈出式視窗於最上方" } } } }, "Pin version" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Version anheften" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Fijar versión" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Épingler la version" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Sematkan versi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Versione fissa" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "バージョンを固定" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "버전 고정" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Przypnij wersję" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Fixar versão" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Fixar versão" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Закрепить версию" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pripnúť verziu" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pripni različico" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sürümü sabitle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Закріпити версію" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ghim phiên bản" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "固定版本" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "釘選版本" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "釘選版本" } } } }, "Pip Packages" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Pip-Pakete" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Paquetes Pip" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Paquets Pip" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Paket Pip" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Pacchetti Pip" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Pip パッケージ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Pip 패키지" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pakiety Pip" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Pacotes Pip" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Pacotes Pip" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Пакеты Pip" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Balíky Pip" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pip paketi" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Pip Paketleri" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Пакети Pip" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Gói Pip" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Pip 包" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Pip 套件" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "Pip 套件" } } } }, "Please install the helper service" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Bitte installieren Sie den Hilfsdienst" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Instale el servicio auxiliar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Veuillez installer le service d'autorisation" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Silakan pasang layanan pembantu" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Si prega di installare il servizio di supporto" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ヘルパーサービスをインストールしてください" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "도우미 서비스를 설치하세요" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Proszę zainstalować narzędzie pomocnicze" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Por favor, instale o serviço auxiliar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Por favor, instale o serviço auxiliar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Пожалуйста, установите вспомогательную службу" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nainštalujte prosím pomocnú službu" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prosimo, namestite pomožno storitev" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Lütfen yardımcı servisi yükleyin" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Будь ласка, встановіть допоміжний сервіс" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Vui lòng cài đặt dịch vụ phụ trợ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "请安装辅助服务" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "請安裝輔助應用程式服務" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "請安裝輔助應用程式服務" } } } }, "Plist File" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Plist Datei" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Archivo Plist" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Fichier Plist" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "File Plist" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "File Plist" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Plistファイル" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Plist 파일" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Plik Plist" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Arquivo Plist" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ficheiro Plist" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Файл Plist" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Plist súbor" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Plist datoteka" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Plist Dosyası" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Файл Plist" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tệp Plist" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Plist 文件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "PLIST 文件" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "PLIST 文件" } } } }, "Plist: %@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Plist: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Plist: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Plist: %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Plist: %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Plist: %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Plist: %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Plist: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Plist: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Plist: %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Plist: %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Plist: %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Plist: %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Plist: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Plist: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Plist: %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tệp Plist: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "属性列表:%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Plist:%@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "Plist:%@" } } } }, "Plugin Manager" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Plugin-Manager" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Administrador de complementos" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Gestionnaire de plugins" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pengelola Plugin" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Gestore plugin" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "プラグインマネージャー" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "플러그인 관리자" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Menedżer wtyczek" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Gerenciador de Plugins" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Gestor de Plugins" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Менеджер плагинов" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Správca pluginov" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Upravitelj vtičnikov" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Eklenti Yöneticisi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Менеджер плагінів" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Trình quản lý Plugin" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "插件管理器" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "插件管理器" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "插件管理器" } } } }, "Plugins" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Plugins" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Plugins" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Plugins" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Plugin" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Plugin" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "プラグイン" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "플러그인" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wtyczki" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Plugins" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Plugins" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Плагины" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pluginy" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Vtičniki" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Eklentiler" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Плагіни" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Các plugin" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "插件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "插件" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "插件" } } } }, "Potential savings from bundle thinning" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Potenzielle Einsparungen durch Paketdünnung" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ahorros potenciales por reducción de paquetes" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Économies potentielles grâce à l'allègement du bundle" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Potensi penghematan dari pengurangan bundel" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Risparmi potenziali dal bundle thinning" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "バンドル削減による潜在的な節約" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "번들 축소로 인한 잠재적인 절약" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Potencjalne oszczędności wynikające z zmniejszenia rozmiaru pakietu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Economia potencial com a redução do pacote" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Poupanças potenciais com a redução de pacotes" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Потенциальная экономия от уменьшения пакета" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Potenciálne úspory z redukcie balíka" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Potencialni prihranki pri zmanjšanju paketa" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Paket inceltmeden elde edilebilecek tasarruflar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Потенційна економія від зменшення пакету" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tiềm năng tiết kiệm từ việc giảm gói" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "通过捆绑精简实现的潜在节省" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "從套件精簡中節省的潛力" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "從精簡套裝中潛在節省" } } } }, "Pre-release" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Vorabversion" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Prelanzamiento" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Pré-version" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pra-rilis" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Pre-rilascio" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "プレリリース" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "사전 출시" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wersja przedpremierowa" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Pré-lançamento" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Pré-lançamento" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Предварительный выпуск" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Predbežné vydanie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Predizdaja" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ön sürüm" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Попередній випуск" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Phát hành trước" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "预发布" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "預發佈" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "預發佈" } } } }, "Preferred language" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Bevorzugte Sprache" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Idioma preferido" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Langue préférée" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Bahasa yang disukai" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Lingua preferita" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "優先言語" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "선호 언어" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Preferowany język" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Idioma preferido" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Idioma preferido" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Предпочтительный язык" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Preferovaný jazyk" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prednostni jezik" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Tercih edilen dil" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Бажана мова" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ngôn ngữ ưa thích" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "首选语言" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "首選語言" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "首選語言" } } } }, "Previous action: %@" : { }, "Privileged Helper Not Installed!" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Privilegierter Helfer nicht installiert!" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "¡Asistente privilegiado no instalado!" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Assistant privilégié non installé !" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pembantu Istimewa Belum Terpasang!" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Assistente privilegiato non installato!" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "特権ヘルパーがインストールされていません!" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "권한 상승 도우미가 설치되지 않았습니다!" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nie zainstalowano uprzywilejowanego pomocnika!" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Assistente privilegiado não instalado!" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Assistente Privilegiado Não Instalado!" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Привилегированный помощник не установлен!" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Privilegovaný pomocník nie je nainštalovaný!" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Privilegirani pomočnik ni nameščen!" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ayrıcalıklı Yardımcı Yüklenmedi!" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Привілейований помічник не встановлено!" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Trình trợ giúp đặc quyền chưa được cài đặt!" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "特权助手未安装!" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "未安裝特權助手!" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "特權助手未安裝!" } } } }, "Prune Translations" : { "comment" : "Prune alert title", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "um Übersetzungen kürzen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Podar traducciones" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer des traductions" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pangkas Terjemahan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Elimina traduzioni" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "翻訳を整理" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "언어 제거" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyczyść tłumaczenia" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Podar Traduções" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Podar Traduções" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Очистить переводы" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstrániť preklady" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Obreži prevode" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Çevirileri Temizle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Очищення перекладів" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Cắt bớt bản dịch" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "删除翻译" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "刪除翻譯" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "刪除翻譯" } } } }, "Purgeable space refers to the System Data taken up by macOS. This cannot be manually freed and is automatically managed by your system." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Bereinigbarer Speicher bezieht sich auf die Systemdaten, die von macOS belegt werden. Dieser kann nicht manuell freigegeben werden und wird automatisch von Ihrem System verwaltet." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "El espacio purgable se refiere a los Datos del Sistema ocupados por macOS. Esto no se puede liberar manualmente y es gestionado automáticamente por tu sistema." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "L'espace purgeable fait référence aux données système occupées par macOS. Cela ne peut pas être libéré manuellement et est géré automatiquement par votre système." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ruang yang dapat dibersihkan mengacu pada Data Sistem yang digunakan oleh macOS. Ini tidak dapat dibebaskan secara manual dan dikelola secara otomatis oleh sistem Anda." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Lo spazio eliminabile si riferisce ai Dati di Sistema occupati da macOS. Questo non può essere liberato manualmente ed è gestito automaticamente dal tuo sistema." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "パージ可能なスペースとは、macOSによって占有されるシステムデータを指します。これは手動で解放することはできず、システムによって自動的に管理されます。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "정리 가능한 공간은 macOS가 차지하는 시스템 데이터를 의미합니다. 이는 수동으로 해제할 수 없으며 시스템에 의해 자동으로 관리됩니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Przestrzeń do wyczyszczenia odnosi się do danych systemowych zajmowanych przez system macOS. Nie można jej zwolnić ręcznie i jest ona automatycznie zarządzana przez system." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "O espaço apagável refere-se aos Dados do Sistema ocupados pelo macOS. Isso não pode ser liberado manualmente e é gerenciado automaticamente pelo seu sistema." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "O espaço purgável refere-se aos Dados do Sistema ocupados pelo macOS. Isto não pode ser libertado manualmente e é gerido automaticamente pelo seu sistema." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Очищаемое пространство относится к системным данным, занимаемым macOS. Это пространство нельзя освободить вручную, оно автоматически управляется вашей системой." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vymazateľné miesto sa vzťahuje na systémové dáta obsadené macOS. Toto nemôže byť manuálne uvoľnené a je automaticky spravované vaším systémom." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Očiščljiv prostor se nanaša na sistemske podatke, ki jih zavzema macOS. Tega prostora ni mogoče ročno sprostiti in ga vaš sistem samodejno upravlja." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Silinebilir alan, macOS tarafından kullanılan Sistem Verilerini ifade eder. Bu alan elle boşaltılamaz ve sisteminiz tarafından otomatik olarak yönetilir." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Очищуваний простір відноситься до системних даних, зайнятих macOS. Це не можна звільнити вручну, і система керує цим автоматично." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Dung lượng có thể xóa đề cập đến Dữ liệu Hệ thống do macOS chiếm dụng. Điều này không thể được giải phóng thủ công và được hệ thống của bạn tự động quản lý." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "可清除空间是指被macOS占用的系统数据。这无法手动释放,由系统自动管理。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "可清除空間是指 macOS 佔用的系統數據。這無法手動釋放,並由系統自動管理。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "可清除空間是指由 macOS 佔用的系統數據。這不能手動釋放,並由系統自動管理。" } } } }, "Purgeable:" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Löschbar:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Purgable:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Purgeable :" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Dapat Dihapus:" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Eliminabile:" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "削除可能:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "정리 가능: " } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Możliwość czyszczenia:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Purgável:" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Purgável:" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Подлежащий очистке:" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vyčistiteľné:" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Očiščljivo:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Temizlenebilir:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Можна очистити:" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Có thể xóa:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "可清除" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "可清除:" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "可清除的" } } } }, "Python:" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Python:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Python:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Python:" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Python:" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Python:" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Python:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Python:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Python:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Python:" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Python:" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Python:" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Python:" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Python:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Python:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Python:" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Python:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Python:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Python:" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "Python:" } } } }, "Quit" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Beenden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Salir" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Quitter" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Keluar" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Esci" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "終了" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "종료" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zamknij" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Sair" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Sair" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Выйти" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Ukončiť" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izhod" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Çık" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Закрити" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thoát" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "退出" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "結束" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "結束" } } } }, "Quit Pearcleaner" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner beenden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Salir de Pearcleaner" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Quitter Pearcleaner" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Keluar Pearcleaner" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Esci da Pearcleaner" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleanerを終了" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner 종료" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zamknij program Pearcleaner" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Sair do Pearcleaner" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Sair do Pearcleaner" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Выйти из Pearcleaer" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Ukončiť Pearcleaner" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izhod iz Pearcleaner" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner’dan Çık" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Закрити Pearcleaner" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thoát Pearcleaner" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "退出 Pearcleaner" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "結束 Pearcleaner" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "結束 Pearcleaner" } } } }, "Ready to search" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Bereit zur Suche" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Listo para buscar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Prêt à rechercher" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Siap untuk mencari" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Pronto per la ricerca" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "検索の準備ができました" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "검색 준비 완료" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Gotowy do wyszukiwania" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Pronto para pesquisar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Pronto para pesquisar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Готов к поиску" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pripravený na vyhľadávanie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pripravljen za iskanje" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Aramaya hazır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Готовий до пошуку" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Sẵn sàng để tìm kiếm" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "准备搜索" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "準備搜尋" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "準備搜尋" } } } }, "Real" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Tatsächlich" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Real" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Réel" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Nyata" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Reale" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "実際のサイズ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "리얼" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Rzeczywiste" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Real" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Реальный" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Reálna" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Realno" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Gerçek" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Справжній" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Kích thước thực (Real size)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "真实大小" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "實際大小" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "實際大小" } } } }, "Real size type will show how much actual allocated space the file has on disk.\n\nLogical type shows the binary size. The filesystem can compress and deduplicate sectors on disk, so real size is sometimes smaller(or bigger) than logical size.\n\nFinder size is similar to if you right click > Get Info on a file in Finder, which will show both the logical and real sizes together." : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Die tatsächliche Grösse gibt an, wie viel Platz die Datei auf der Festplatte belegt.\n\nDer logische Typ zeigt die binäre Grösse an. Das Dateisystem kann Sektoren auf der Festplatte komprimieren und deduplizieren, so dass die reale Grösse manchmal kleiner (oder größer) als die logische Grösse ist.\n\nDie Finder-Grösse ist vergleichbar mit einem Rechtsklick auf eine Datei im Finder, der sowohl die logische als auch die reale Grösse anzeigt." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "El tipo de tamaño real mostrará cuánto espacio realmente asignado tiene el archivo en el disco\n\nEl tipo lógico muestra el tamaño binario. El sistema de archivos puede comprimir y deduplicar sectores en el disco, por lo que el tamaño real a veces es más pequeño (o más grande) que el tamaño lógico\n\nEl tamaño del Finder es similar a si haces clic derecho > Obtener información sobre un archivo en Finder, lo que mostrará tanto los tamaños lógicos como reales juntos" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Le type de taille réelle indique l'espace réellement alloué au fichier sur le disque.\n\nLe type logique montre la taille binaire. Le système de fichiers peut compresser et dédupliquer les secteurs sur le disque, de sorte que la taille réelle est parfois plus petite (ou plus grande) que la taille logique.\n\nLa taille du Finder est similaire à celle que vous obtiendriez en faisant un clic droit > Lire les informations sur un fichier dans le Finder, qui affichera les tailles logique et réelle ensemble.\n" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Jenis ukuran nyata akan menunjukkan berapa banyak ruang yang sebenarnya dialokasikan oleh file di disk.\n\nJenis logis menunjukkan ukuran biner. Sistem file dapat mengompresi dan menduplikasi sektor di disk, jadi ukuran nyata terkadang lebih kecil (atau lebih besar) daripada ukuran logis.\n\nUkuran Finder mirip dengan jika Anda klik kanan > Dapatkan Info pada file di Finder, yang akan menampilkan ukuran logis dan nyata secara bersamaan." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Il tipo di dimensione reale mostrerà quanto spazio effettivo è allocato sul disco per il file.\n\nIl tipo logico mostra la dimensione binaria. Il filesystem può comprimere e deduplicare i settori sul disco, quindi la dimensione reale è talvolta più piccola (o più grande) della dimensione logica.\n\nLa dimensione del Finder è simile a quando si fa clic con il tasto destro > Ottieni informazioni su un file nel Finder, che mostrerà sia le dimensioni logiche che quelle reali insieme." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "「実際のサイズ」タイプは、ファイルがディスク上で実際に割り当てられているスペースの量を表示します。\n\n「論理サイズ」タイプは、バイナリサイズを表示します。ファイルシステムはディスク上のセクターを圧縮し、重複を排除することができるため、実際のサイズは論理サイズより小さい(または大きい)場合があります。\n\n「Finder サイズ」は、Finder でファイルを右クリックして「情報を取得」を選択した場合と同様で、論理サイズと実際のサイズの両方を表示します。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "실제 크기 유형은 파일이 디스크에 할당된 실제 공간의 크기를 보여줍니다.\n\n논리적 유형은 이진 크기를 보여줍니다. 파일 시스템은 디스크에서 섹터를 압축하고 중복 제거할 수 있으므로 실제 크기는 때때로 논리적인 크기보다 작거나 큽니다.\n\nFinder 크기는 Finder에서 파일을 마우스 오른쪽 버튼으로 클릭하여 '정보 가져오기'를 선택했을 때와 유사하며, 논리적 크기와 실제 크기를 함께 보여줍니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Typ rzeczywistego rozmiaru pokazuje, ile miejsca na dysku zajmuje plik.\n\nTyp logiczny pokazuje rozmiar binarny. System plików może kompresować i deduplikować sektory na dysku, więc rzeczywisty rozmiar jest czasem mniejszy (lub większy) niż rozmiar logiczny.\n\nRozmiar w Finderze jest podobny do tego, który można zobaczyć po kliknięciu prawym przyciskiem myszy > Informacje o pliku w Finderze, gdzie pokazany jest zarówno rozmiar logiczny, jak i rzeczywisty." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "O tipo Tamanho real mostrará quanto espaço alocado o arquivo ocupa no disco. O tipo Lógico exibe o tamanho binário. O sistema de arquivos pode comprimir e desduplicar os setores no disco, então o tamanho real às vezes é menor (ou maior) que o tamanho lógico. O tamanho no Finder é semelhante ao clicar com o botão direito > Obter Informações em um arquivo no Finder, que exibirá os tamanhos lógico e real juntos." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "O tipo de tamanho real mostrará quanto espaço alocado o ficheiro tem no disco.\n\nO tipo lógico mostra o tamanho binário. O sistema de ficheiros pode comprimir e deduplicar setores no disco, por isso o tamanho real é por vezes menor (ou maior) do que o tamanho lógico.\n\nO tamanho do Finder é semelhante a clicar com o botão direito > Obter Informações num ficheiro no Finder, que mostrará os tamanhos lógico e real juntos." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Реальный размер показывает фактическое место, занятое файлом на диске. \n\nЛогический размер отражает размер бинарного файла. Файловая система может сжимать или удалять дубликаты секторов, поэтому реальный размер может быть меньше или больше логического. \n\nРазмер в Finder соответствует данным, отображаемым при выборе 'Свойства' в Finder, где показаны оба размера вместе." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Reálna veľkosť zobrazí, koľko alokovaného miesta má súbor skutočne na disku.\n\nLogická veľkosť zobrazuje binárnu veľkosť. Súborový systém môže komprimovať a deduplikovať sektory na disku, takže reálna veľkosť je niekedy menšia (alebo väčšia) ako logická veľkosť.\n\nVeľkosť podľa Finderu je podobná, ako keď klikneš pravým tlačidlom myši na súbor vo Finderi a vyberieš možnosť \"Informácie\", čo zobrazí logickú aj reálnu veľkosť spolu." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Tip dejanske velikosti bo pokazal, koliko dejansko dodeljenega prostora ima datoteka na disku.\n\nLogični tip prikazuje binarno velikost. Datotečni sistem lahko stisne in deduplicira sektorje na disku, zato je dejanska velikost včasih manjša (ali večja) od logične velikosti.\n\nVelikost Finderja je podobna, kot če z desno tipko kliknete > Pridobi informacije na datoteki v Finderju, kar bo pokazalo tako logične kot dejanske velikosti skupaj." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Gerçek boyut türü, dosyanın diskte ne kadar gerçek ayrılmış alana sahip olduğunu gösterir. \n\nMantıksal tür ikili boyutu gösterir. Dosya sistemi disk üzerindeki sektörleri sıkıştırabilir ve tekilleştirebilir, bu nedenle gerçek boyut bazen mantıksal boyuttan daha küçük (veya daha büyük) olabilir.\n\nFinder boyutu, Finder'da bir dosyaya sağ tıklayıp > Bilgi Al dediğinizde hem mantıksal hem de gerçek boyutların birlikte gösterilmesine benzer." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Справжній розмір показує, скільки реально виділено місця на диску для файлу.\n\nЛогічний тип показує двійковий розмір. Файлова система може стискати і дедуплікувати сектори на диску, тому реальний розмір іноді може бути меншим (або більшим) за логічний.\n\nРозмір Finder подібний до того, якби ви клацнули правою кнопкою миші > Отримати інформацію про файл у Finder, де буде показано як логічний, так і реальний розмір разом." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Kiểu kích thước thực (Real size) sẽ hiển thị dung lượng thực tế mà tệp chiếm dụng trên ổ đĩa.\n\nKiểu kích thước logic (Logical type) hiển thị kích thước nhị phân của tệp. Hệ thống tệp có thể nén và loại bỏ trùng lặp các vùng lưu trữ trên ổ đĩa, vì vậy kích thước thực đôi khi có thể nhỏ hơn (hoặc lớn hơn) kích thước logic.\n\nKích thước Finder tương tự như khi bạn nhấp chuột phải vào tệp > chọn “Lấy thông tin” (Get Info) trong Finder, nơi sẽ hiển thị cả kích thước logic và kích thước thực của tệp." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "真实大小类型将显示文件在磁盘上实际分配的空间大小。\n\n逻辑类型显示二进制大小。文件系统可以压缩和去重磁盘上的扇区,因此真实大小有时小于(或大于)逻辑大小。\n\nFinder大小类似于在Finder中右键单击 > 获取信息,这将同时显示逻辑和真实大小。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "「實際大小」種類顯示檔案在磁碟上實際分配了多少空間。\n\n「邏輯大小」種類顯示檔案的二進位大小。二進位檔案系統可以在磁碟上壓縮並减少重複的磁區,因此實際大小有時會比邏輯大小更小(或更大)。\n\n「Finder 大小」與你在 Finder 中點按右鍵 > 取得資料的動作類似,這會讓邏輯大小與實際大小在一起顯示。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "「實際大小」種類顯示檔案在磁碟上實際分配了多少空間。\n\n「邏輯大小」種類顯示檔案的二進位大小。二進位檔案系統可以在磁碟上壓縮並减少重複的磁區,因此實際大小有時會比邏輯大小更小(或更大)。\n\n「Finder 大小」與你在 Finder 中點按右鍵 > 取得資料的動作類似,這會讓邏輯大小與實際大小在一起顯示。" } } } }, "Reason: %@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Grund: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Razón: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Raison : %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Alasan: %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Motivo: %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "理由: %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이유: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Powód: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Razão: %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Razão: %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Причина: %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Dôvod: %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Razlog: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sebep: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Причина: %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Lý do: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "原因:%@ " } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "原因:%@ " } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "原因:%@ " } } } }, "Receipt Path:" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Pfad zum Beleg:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ruta del Recibo:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Chemin d’accès du récepteur" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Jalur Tanda Terima:" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Percorso della Ricevuta:" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "領収書のパス:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "영수증 경로:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ścieżka odbioru:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Caminho do Recibo:" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Caminho do Recibo:" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Путь к квитанции:" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Cesta k účtenke:" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pot prejema:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Makbuz Yolu:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Шлях до квитанції:" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đường dẫn biên nhận:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "收据路径:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "收據路徑:" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "收據路徑:" } } } }, "Receipt Storage Paths (%lld)" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Belegspeicherpfade (%lld)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Rutas de almacenamiento de recibos (%lld)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Chemins de stockage des reçus (%lld)" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Jalur Penyimpanan Tanda Terima (%lld)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Percorsi di archiviazione delle ricevute (%lld)" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "領収書保存パス (%lld)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "영수증 저장 경로 (%lld)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ścieżki przechowywania paragonów (%lld)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Caminhos de Armazenamento de Recibos (%lld)" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Caminhos de Armazenamento de Recibos (%lld)" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Пути хранения квитанций (%lld)" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Cesty úložiska potvrdení (%lld)" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Poti za shranjevanje potrdil (%lld)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Makbuz Depolama Yolları (%lld)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Шляхи зберігання квитанцій (%lld)" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đường dẫn lưu trữ biên lai (%lld)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "收据存储路径 (%lld)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "收據儲存路徑 (%lld)" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "收據儲存路徑 (%lld)" } } } }, "Recommended Dependencies" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Empfohlene Abhängigkeiten" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Dependencias Recomendadas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Dépendances Recommandées" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ketergantungan yang Direkomendasikan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Dipendenze consigliate" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "推奨される依存関係" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "추천 종속성" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zalecane zależności" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Dependências Recomendadas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Dependências Recomendadas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Рекомендуемые зависимости" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odporúčané závislosti" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Priporočene odvisnosti" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Önerilen Bağımlılıklar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Рекомендовані залежності" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Các Phụ Thuộc Được Đề Xuất" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "推荐的依赖项" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "推薦的依賴項" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "推薦的依賴項" } } } }, "Refresh" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Erneuern" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualizar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Rafraîchir" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Segarkan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiorna" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "更新" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "새로 고침" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Odśwież" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Обновить" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Obnoviť" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Osveži" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yenile" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оновити" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Làm mới" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "刷新" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "重新整理" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "重新整理" } } } }, "Refresh %@ data" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Aktualisiere %@ Daten" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualizar datos de %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Actualiser les données %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Segarkan data %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiorna dati di %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%@ のデータを更新" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@ 데이터 새로 고침" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Odśwież dane %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar dados de %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar dados de %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Обновить данные %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Obnoviť %@ údaje" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Osveži %@ podatke" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%@ verilerini yenile" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оновити дані %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Làm mới dữ liệu %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "刷新 %@ 数据" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "刷新 %@ 資料" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "刷新 %@ 資料" } } } }, "Refresh Apps" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Erneuere Programme" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualizar aplicaciones" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Rafraîchir les applications" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Segarkan Aplikasi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiorna app" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アプリを更新" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "앱 새로 고침" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Odśwież aplikacje" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar Apps" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar Apps" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Обновить приложения" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Obnoviť aplikácie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Osveži aplikacije" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uygulamaları Yenile" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оновити Програми" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Làm mới ứng dụng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "刷新应用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "重新整理 App" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "重新整理 App" } } } }, "Refresh apps (⌘+R)" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Erneuere Programme (⌘+R)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualizar aplicaciones (⌘+R)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Rafraîchir les applications (⌘+R)" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Segarkan aplikasi (⌘+R)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiorna app (⌘+R)" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アプリを更新(⌘R)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "앱 새로 고침 (⌘+R)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Odśwież aplikacje (⌘+R)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar apps (⌘+R)" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar aplicações (⌘+R)" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Обновить приложения (⌘+R)" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Obnoviť aplikácie (⌘+R)" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Osveži aplikacije (⌘+R)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uygulamaları yenile (⌘+R)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оновити Програми (⌘+R)" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Làm mới ứng dụng (⌘+R)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "刷新应用 (⌘+R)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "重新整理 App(⌘R)" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "重新整理 App(⌘R)" } } } }, "Refresh List" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Liste aktualisieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualizar lista" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Actualiser la liste" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Segarkan Daftar" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiorna elenco" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "リストを更新" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "목록 새로 고침" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Odśwież liste" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar Lista" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar Lista" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Обновить список" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizovať zoznam" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Osveži seznam" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Listeyi Yenile" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оновити список" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Làm mới danh sách" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "刷新列表" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "重新整理清單" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "重新整理列表" } } } }, "Refresh updater" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Erneuere Updater" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualizar el actualizador" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Rafraîchir et vérifier si une mise à jour est disponible" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Segarkan pembaru" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiorna aggiornamento" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アップデーターを更新" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "업데이터 새로 고침" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Odśwież aktualizator" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar atualizador" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar atualizador" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Проверить обновления" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Obnoviť aktualizátor" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Osveži posodabljalnik" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Güncelleyiciyi yenile" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Освіжити оновлювач" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Làm mới trình cập nhật" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "刷新更新程序" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "重新整理更新程式" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "重新整理更新程式" } } } }, "Refreshing..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Aktualisieren..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualizando..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Actualisation..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Menyegarkan..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiornamento in corso..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "更新中..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "새로 고치는 중..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Odświeżanie..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualizando..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A atualizar..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Обновление..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Obnovujem..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Osveževanje..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yenileniyor..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оновлення..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang làm mới..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "正在刷新..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "正在刷新..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "正在更新..." } } } }, "Registered but requires enabling in System Settings > Login Items." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Registriert, muss jedoch in den Systemeinstellungen > Anmeldeobjekte aktiviert werden." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Registrado pero requiere habilitación en Configuración del Sistema > Elementos de inicio de sesión." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Bien enregistré(e) mais doit être activé(e) dans Registered but requires enabling in System Settings > Login Items." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Terdaftar tetapi memerlukan pengaktifan di Pengaturan Sistem > Item Login" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Registrato ma richiede l'abilitazione in Impostazioni di Sistema > Elementi di Login." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "登録済みですが、システム設定 > ログイン項目で有効化が必要です。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "등록되었지만 시스템 설정 > 로그인 항목에서 활성화가 필요합니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zarejestrowany, ale wymaga włączenia w Ustawieniach systemowych > Rzeczy i rozszerzenia otwierane podczas logowania." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Registrado, mas precisa ser ativado em Ajustes do Sistema > Itens de Início." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Registado, mas requer ativação em Definições do Sistema > Itens de Início de Sessão." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Зарегистрировано, но требует включения в Системных настройках > Элементы входа." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Registrované, ale vyžaduje povolenie v Systémových nastaveniach > Položky pri prihlásení" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Registrirano, vendar zahteva omogočanje v Sistemske nastavitve > Prijavni elementi." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kayıtlı, ancak Sistem Ayarları > Oturum Açma Öğeleri'nde etkinleştirilmesi gerekiyor." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Зареєстровано, але потребує увімкнення в Системних налаштуваннях > Елементи входу." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đã đăng ký nhưng cần bật trong Cài đặt hệ thống > Mục đăng nhập." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "已注册但需要在系统设置>登录项中启用。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "已註冊,但需要在「系統設定」>「登入項目」中啟用。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "已註冊,但需要在「系統設定」>「登入項目」中啟用。" } } } }, "Regular" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Regulär" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Regular" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Régulier" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Reguler" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Regolare" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "レギュラー" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "일반" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Regularny" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Regular" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Regular" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Обычный" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pravidelný" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Redno" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Normal" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Звичайний" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thường xuyên" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "常规" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "常規" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "常規" } } } }, "Release Notes" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Versionshinweise" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Notas de la versión" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Notes de version" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Catatan Rilis" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Note di rilascio" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "リリースノート" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "릴리스 노트" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Informacje o wydaniu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Notas de Lançamento" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Notas de Lançamento" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Примечания к выпуску" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Poznámky k vydaniu" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Opombe ob izdaji" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sürüm Notları" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Примітки до релізу" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ghi chút phát hành" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "更新日志" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "版本備註" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "版本備註" } } } }, "Releases" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Versionen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Versiones" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Versions" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Rilis" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Rilasci" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "リリース" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "릴리즈" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wydania" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Versões" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Lançamentos" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Выпуски" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vydania" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izdaje" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sürümler" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Релізи" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Các bản cập nhật" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "发布" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "發行版本" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "發行版本" } } } }, "Remaining files and folders from previous applications" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Verbleibende Dateien und Ordner aus früheren Anwendungen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Archivos y carpetas restantes de aplicaciones anteriores" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Fichiers et dossiers restants des applications précédemment installées " } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "File dan folder yang tersisa dari aplikasi sebelumnya" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "File e cartelle rimanenti dalle applicazioni precedenti" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "以前のアプリケーションからの残りのファイルとフォルダー" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이전 애플리케이션의 남은 파일 및 폴더" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pliki i foldery pozostałe po poprzednich aplikacjach" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Arquivos e pastas restantes de aplicativos anteriores" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ficheiros e pastas restantes de aplicações anteriores" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Оставшиеся файлы и папки из предыдущих приложений" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zostávajúce súbory a priečinky z predchádzajúcich aplikácií" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Preostale datoteke in mape iz prejšnjih aplikacij" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Önceki uygulamalardan kalan dosya ve klasörler" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Залишки файлів і папок з попередніх програм" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Các tệp và thư mục còn lại từ các ứng dụng trước đó" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "来自先前应用程序的剩余文件和文件夹" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "之前應用程式的剩餘檔案和資料夾" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "之前應用程式的剩餘檔案和資料夾" } } } }, "Remove" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Entfernen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Rimuovi" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "削除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "제거" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Remover" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Remover" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstrániť" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odstrani" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kaldır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Gỡ bỏ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "移除" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "移除" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "移除" } } } }, "Remove %@?" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "%@ entfernen?" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "¿Eliminar %@?" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer %@?" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus %@?" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Rimuovere %@?" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%@を削除しますか?" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@을(를) 제거하시겠습니까?" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usunąć %@?" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Remover %@?" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Remover %@?" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить %@?" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstrániť %@?" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odstraniti %@?" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%@ kaldır?" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити %@?" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa %@?" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "移除 %@?" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "移除 %@?" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "移除 %@?" } } } }, "Remove All" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Alle entfernen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar todo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Tout supprimer" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus Semua" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Rimuovi Tutto" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "すべて削除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "모두 제거" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń wszystko" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Remover Tudo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Remover Tudo" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить все" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstrániť všetko" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odstrani vse" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hepsini Kaldır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити все" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa Tất Cả" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "全部移除" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "全部移除" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "全部移除" } } } }, "Remove all files/folders" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Alle Dateien/Ordner entfernen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar todos los archivos/carpetas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer tous les fichiers ou répertoires" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus semua file/folder" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Rimuovi tutti i file/cartelle" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "すべてのファイル/フォルダを削除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "모든 파일/폴더 제거" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń wszystkie pliki/foldery" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Remover todos os arquivos/pastas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Remover todos os ficheiros/pastas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить все файлы/папки" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstrániť všetky súbory/zložky" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odstrani vse datoteke/mape" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Tüm dosyaları/klasörleri kaldır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити всі файли/папки" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Loại bỏ tất cả các tệp/thư mục" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "删除所有文件/文件夹" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "移除全部檔案/資料夾" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "移除全部檔案/資料夾" } } } }, "Remove association and exclusion" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Zuweisung und Ausschluss entfernen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar asociación y exclusión" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer l'association et l'exclusion" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus asosiasi dan pengecualian" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Rimuovi associazione ed esclusione" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "関連付けと除外を削除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "연관 및 제외 제거" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń powiązanie i wykluczenie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Remover associação e exclusão" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Remover associação e exclusão" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить ассоциацию и исключение" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstrániť asociáciu a vylúčenie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odstrani povezavo in izključitev" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Birleştirme ve hariç tutmayı kaldır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Вилучити асоціацію та виняток" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa liên kết và loại trừ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "删除关联和排除" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "移除關聯和排除" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "移除關聯和排除" } } } }, "Remove folder" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ordner entfernen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar carpeta" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer le dossier" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus folder" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Rimuovi cartella" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "フォルダーを削除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "폴더 제거" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń folder" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Remover pasta" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Remover pasta" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить папку" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstrániť priečinok" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odstrani mapo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Klasörü kaldır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити папку" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa thư mục" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "删除文件夹" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "移除資料夾" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "移除資料夾" } } } }, "Remove from both lists" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Aus beiden Listen entfernen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar de ambas listas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer des deux listes" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus dari kedua daftar" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Rimuovi da entrambe le liste" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "両方のリストから削除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "두 목록에서 제거" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń z obu list" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Remover de ambas as listas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Remover de ambas as listas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить из обоих списков" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstrániť z oboch zoznamov" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odstrani z obeh seznamov" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Her iki listeden de kaldır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити з обох списків" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa khỏi cả hai danh sách" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "从两个列表中移除" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "從兩個列表中刪除" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "從兩個列表中移除" } } } }, "Remove from exclusion list" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Aus Ausschlussliste entfernen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar de la lista de exclusión" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer d’après la liste d’exclusions" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus dari daftar pengecualian" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Rimuovi dalla lista di esclusione" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "除外リストから削除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "제외 목록에서 제거" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń z listy wykluczeń" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Remover da lista de exclusão" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Remover da lista de exclusão" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить из списка исключений" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstrániť zo zoznamu výnimiek" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odstrani s seznama izjem" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hariç tutma listesinden çıkar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити зі списку виключень" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Gỡ khỏi danh sách loại trừ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "从排除列表中移除" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "從排除列表中移除" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "從排除清單中移除" } } } }, "Remove orphaned dependencies, old package versions and scrub download cache" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Verwaiste Abhängigkeiten, alte Paketversionen entfernen und Download-Cache bereinigen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar dependencias huérfanas, versiones antiguas de paquetes y limpiar la caché de descargas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer les dépendances orphelines, les anciennes versions de paquets et nettoyer le cache de téléchargement" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus dependensi yatim piatu, versi paket lama, dan bersihkan cache unduhan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Rimuovi dipendenze orfane, vecchie versioni dei pacchetti e pulisci la cache dei download" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "孤立した依存関係、古いパッケージバージョンを削除し、ダウンロードキャッシュを消去する" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "연결되지 않은 종속성, 오래된 패키지 버전을 제거하고 다운로드 캐시를 정리합니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń osierocone zależności, stare wersje pakietów i wyczyść pamięć podręczną pobierania" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Remover dependências órfãs, versões antigas de pacotes e limpar o cache de download" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Remover dependências órfãs, versões antigas de pacotes e limpar a cache de downloads" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить осиротевшие зависимости, старые версии пакетов и очистить кэш загрузок" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstrániť osirelé závislosti, staré verzie balíkov a vyčistiť vyrovnávaciu pamäť sťahovania" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odstrani osirotele odvisnosti, stare različice paketov in počisti predpomnilnik prenosa" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yetim kalmış bağımlılıkları, eski paket sürümlerini kaldır ve indirme önbelleğini temizle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити осиротілі залежності, старі версії пакетів і очистити кеш завантажень" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa các phụ thuộc mồ côi, phiên bản gói cũ và xóa bộ nhớ cache tải xuống" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "删除孤立的依赖项、旧的软件包版本并清除下载缓存" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "刪除孤立的依賴項、舊的軟件包版本並清除下載緩存" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "移除孤立的依賴項、舊的軟件包版本並清除下載緩存" } } } }, "Remove package from system records (does not delete files)" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Paket aus Systemaufzeichnungen entfernen (löscht keine Dateien)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar el paquete de los registros del sistema (no elimina archivos)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer le package des enregistrements système (ne supprime pas les fichiers)" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus paket dari catatan sistem (tidak menghapus file)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Rimuovi pacchetto dai record di sistema (non elimina i file)" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "システム記録からパッケージを削除(ファイルは削除されません)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "시스템 기록에서 패키지 제거 (파일 삭제 안 함)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń pakiet z rejestrów systemu (nie usuwa plików)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Remover pacote dos registros do sistema (não exclui arquivos)" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Remover pacote dos registros do sistema (não exclui arquivos)" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить пакет из системных записей (не удаляет файлы)" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstrániť balík z evidencie systému (neodstráni súbory)" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odstrani paket iz sistemskih zapisov (ne izbriše datotek)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Paketi sistem kayıtlarından kaldır (dosyaları silmez)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Вилучити пакет із системних записів (файли не видаляються)" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa gói khỏi hồ sơ hệ thống (không xóa tệp)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "从系统记录中移除包(不会删除文件)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "從系統記錄中移除套件(不會刪除檔案)" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "從系統記錄中移除套件(不刪除檔案)" } } } }, "Remove Selected" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ausgewähltes entfernen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar seleccionado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer la sélection" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus yang Dipilih" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Rimuovi selezionato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "選択を削除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "선택 항목 제거" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń zaznaczone" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Remover Selecionado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Remover Selecionado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить выбранное" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstrániť vybrané" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odstrani izbrano" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Seçilenleri Kaldır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити вибране" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa đã chọn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "移除选定" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "移除選取" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "移除選擇" } } } }, "Remove tap" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Tippen entfernen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar toque" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer le tapotement" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus ketukan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Rimuovi tocco" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "タップを削除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "탭 제거" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń stuknięcie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Remover toque" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Remover tap" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить нажатие" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstrániť klepnutie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odstrani tap" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Dokunmayı kaldır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити натискання" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa nhấn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "移除点击" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "移除點擊" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "移除點擊" } } } }, "Remove the service registration" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Entfernen Sie die Dienstregistrierung" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar el registro del servicio" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer l'enregistrement du service" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus pendaftaran layanan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Rimuovi la registrazione del servizio" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "サービス登録を削除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "서비스 등록 제거" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń rejestrację usługi" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Remover o registro do serviço" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Remover o registo do serviço" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить регистрацию службы" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstráňte registráciu služby" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odstrani registracijo storitve" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hizmet kaydını kaldır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити реєстрацію сервісу" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Gỡ bỏ đăng ký dịch vụ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "删除服务注册" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "移除服務註冊" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "移除服務註冊" } } } }, "Remove unused architectures from your app binaries to reduce app size" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Entfernen Sie nicht verwendete Architekturen aus Ihren App-Binärdateien, um die App-Größe zu reduzieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Elimina arquitecturas no utilizadas de los binarios de tu aplicación para reducir el tamaño de la aplicación" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimez les architectures inutilisées des binaires de votre application pour réduire la taille de l'application" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus arsitektur yang tidak digunakan dari biner aplikasi Anda untuk mengurangi ukuran aplikasi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Rimuovi le architetture non utilizzate dai binari della tua app per ridurre le dimensioni dell'app" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アプリのバイナリから未使用のアーキテクチャを削除してアプリサイズを縮小" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "앱 크기를 줄이기 위해 앱 바이너리에서 사용하지 않는 아키텍처를 제거하세요" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń nieużywane architektury z plików binarnych aplikacji, aby zmniejszyć jej rozmiar" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Remova arquiteturas não utilizadas de seus binários de aplicativos para reduzir o tamanho do aplicativo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Remover arquiteturas não utilizadas dos binários da sua aplicação para reduzir o tamanho da aplicação" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалите неиспользуемые архитектуры из бинарных файлов приложений, чтобы уменьшить размер приложения" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstráňte nepoužívané architektúry z binárnych súborov aplikácie na zmenšenie veľkosti aplikácie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odstranite neuporabljene arhitekture iz binarnih datotek aplikacije, da zmanjšate velikost aplikacije" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uygulama boyutunu azaltmak için kullanılmayan mimarileri uygulama ikili dosyalarınızdan kaldırın" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видаліть невикористовувані архітектури з бінарників вашого додатка, щоб зменшити його розмір" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Loại bỏ kiến trúc không sử dụng khỏi tệp thực thi của ứng dụng để giảm kích thước ứng dụng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "从应用程序二进制文件中移除未使用的架构以减小应用程序大小" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "移除應用程式二進位檔中的未使用架構以減少應用程式大小" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "從應用程式二進制文件中移除未使用的架構以減少應用程式大小" } } } }, "Remove unused languages during lipo" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Nicht verwendete Sprachen während lipo entfernen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar idiomas no utilizados durante lipo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer les langues inutilisées pendant la lipo" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus bahasa yang tidak digunakan selama lipo" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Rimuovi le lingue non utilizzate durante lipo" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "lipo中に未使用の言語を削除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "번들 축소 중 사용하지 않는 언어 제거" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń nieużywane języki podczas lipo" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Remover idiomas não utilizados durante o lipo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Remover idiomas não utilizados durante o lipo" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалите неиспользуемые языки при работе с lipo" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstrániť nepoužívané jazyky počas lipo" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odstrani neuporabljene jezike med lipo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Lipo sırasında kullanılmayan dilleri kaldır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити невикористовувані мови під час lipo" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa các ngôn ngữ không sử dụng trong quá trình lipo" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "在 lipo 时移除未使用的语言" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "在 lipo 過程中移除未使用的語言" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "在 lipo 過程中移除未使用的語言" } } } }, "Remove workspace storage for deleted project folders" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Arbeitsbereichspeicher für gelöschte Projektordner entfernen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminar el almacenamiento del espacio de trabajo para carpetas de proyectos eliminadas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprimer le stockage de l'espace de travail pour les dossiers de projet supprimés" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus penyimpanan ruang kerja untuk folder proyek yang dihapus" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Rimuovi lo spazio di archiviazione per le cartelle di progetto eliminate" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "削除されたプロジェクトフォルダのワークスペースストレージを削除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "삭제된 프로젝트 폴더의 작업 공간 저장소 제거" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuń miejsce przechowywania obszarów roboczych dla usuniętych folderów projektów" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Remover armazenamento do espaço de trabalho para pastas de projetos excluídas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Remover armazenamento de espaço de trabalho para pastas de projeto excluídas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить хранилище рабочей области для удалённых папок проектов" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstrániť úložisko pracovného priestoru pre odstránené priečinky projektov" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odstrani shrambo delovnega prostora za izbrisane projektne mape" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Silinen proje klasörleri için çalışma alanı depolamasını kaldır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити пам'ять робочої області для вилучених папок проекту" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa lưu trữ không gian làm việc cho các thư mục dự án đã xóa" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "删除已删除项目文件夹的工作区存储" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "刪除專案資料夾的工作區儲存空間" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "移除已刪除項目資料夾的工作空間儲存空間" } } } }, "Removes old versions, orphaned dependencies, and all cache files including latest versions" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Entfernt alte Versionen, verwaiste Abhängigkeiten und alle Cache-Dateien einschließlich der neuesten Versionen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Elimina versiones antiguas, dependencias huérfanas y todos los archivos de caché, incluidas las versiones más recientes" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Supprime les anciennes versions, les dépendances orphelines et tous les fichiers de cache, y compris les dernières versions" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Menghapus versi lama, ketergantungan yatim piatu, dan semua file cache termasuk versi terbaru" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Rimuove le vecchie versioni, le dipendenze orfane e tutti i file di cache, comprese le versioni più recenti" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "古いバージョン、孤立した依存関係、および最新バージョンを含むすべてのキャッシュファイルを削除します" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이전 버전, 연결되지 않은 의존성 및 최신 버전을 포함한 모든 캐시 파일을 제거합니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuwa stare wersje, osierocone zależności i wszystkie pliki pamięci podręcznej, w tym najnowsze wersje" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Remove versões antigas, dependências órfãs e todos os arquivos de cache, incluindo as versões mais recentes" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Remove versões antigas, dependências órfãs e todos os ficheiros de cache, incluindo as versões mais recentes" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удаляет старые версии, осиротевшие зависимости и все файлы кеша, включая последние версии" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstraňuje staré verzie, osirelé závislosti a všetky súbory vyrovnávacej pamäte vrátane najnovších verzií" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odstrani stare različice, osirotele odvisnosti in vse predpomnilniške datoteke, vključno z najnovejšimi različicami" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Eski sürümleri, yetim bağımlılıkları ve en son sürümler dahil tüm önbellek dosyalarını kaldırır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видаляє старі версії, осиротілі залежності та всі файли кешу, включаючи останні версії" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xóa các phiên bản cũ, các phụ thuộc mồ côi và tất cả các tệp bộ nhớ cache bao gồm cả các phiên bản mới nhất" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "移除旧版本、孤立的依赖项和所有缓存文件,包括最新版本" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "移除舊版本、孤立的依賴項和所有緩存文件,包括最新版本" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "移除舊版本、孤立的依賴項和所有緩存文件,包括最新版本" } } } }, "Removing..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Entfernen..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Eliminando..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Suppression..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Menghapus..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Rimozione in corso..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "削除中..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "제거 중..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usuwanie..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Removendo..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Removendo..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удаление..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odstraňovanie..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odstranjevanje..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kaldırılıyor..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалення..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang xóa..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "正在移除..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "正在移除..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "正在移除..." } } } }, "Rename" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Umbenennen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Renombrar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Renommer" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ganti Nama" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Rinomina" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "名前を変更" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이름 변경" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zmień nazwę" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Renomear" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Renomear" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Переименовать" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Premenovať" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Preimenuj" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yeniden Adlandır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Перейменувати" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đổi tên" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "重命名" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "重命名" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "重新命名" } } } }, "Rescan" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Neu scannen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Volver a escanear" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Réanalyser" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pindai Ulang" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Riscansiona" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "再スキャン" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "다시 검색" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ponowne skanowanie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Reexaminar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Reexaminar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Повторное сканирование" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Znova skenovať" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ponovno skeniraj" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yeniden Tara" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Пересканувати" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Quét lại" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "重新扫描" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "重新掃描" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "重新掃描" } } } }, "Reset" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Zurücksetzen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Restablecer" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Réinitaliser" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Atur Ulang" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Reimposta" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "リセット" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "재설정" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zresetuj" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Redefinir" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Reiniciar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Сбросить" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Obnoviť" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ponastavi" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sıfırla" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Скинути" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đặt lại" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "重置" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "重設" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "重設" } } } }, "Reset all settings to default" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Auf Standardeinstellungen zurücksetzten" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Restablecer todos los ajustes a los valores predeterminados" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Réinitialiser tous les paramètres par défaut" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Setel ulang semua pengaturan ke default" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Reimposta tutte le impostazioni ai valori predefiniti" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "すべての設定をデフォルトにリセット" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "모든 설정을 기본값으로 재설정" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Przywróć wszystkie ustawienia do wartości domyślnych" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Redefinir todas as configurações para o padrão" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Repor todas as definições para o padrão" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Сбросить все настройки до значений по умолчанию" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Obnoviť všetky nastavenia na pôvodné" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ponastavi vse nastavitve na privzete" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Tüm ayarları varsayılana sıfırla" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Скинути всі налаштування на стандартні" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đặt lại tất cả cài đặt về mặc định" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "重置所有设置为默认" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "重設全部設定為預設值" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "重設全部設定為預設值" } } } }, "Reset App Store (fixes stuck downloads)" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "App Store zurücksetzen (behebt feststeckende Downloads)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Restablecer App Store (soluciona descargas atascadas)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Réinitialiser l'App Store (corrige les téléchargements bloqués)" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Setel Ulang App Store (memperbaiki unduhan yang macet)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Reimposta App Store (risolve i download bloccati)" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "App Storeをリセット(ダウンロードの停止を修正)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "App Store 재설정 (다운로드 중단 문제 해결)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zresetuj App Store (naprawia zablokowane pobieranie)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Redefinir App Store (corrige downloads travados)" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Reiniciar App Store (corrige downloads bloqueados)" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Сбросить App Store (исправляет зависшие загрузки)" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Resetovať App Store (oprava zaseknutých sťahovaní)" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ponastavi App Store (odpravi zataknjene prenose)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "App Store'u Sıfırla (takılı kalan indirmeleri düzeltir)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Скинути App Store (виправлення завислих завантажень)" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đặt lại App Store (sửa lỗi tải xuống bị kẹt)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "重置应用商店(修复卡住的下载)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "重置應用商店(修復卡住的下載)" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "重設應用程式商店(修復卡住的下載)" } } } }, "Reset App Store?" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "App Store zurücksetzen?" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "¿Restablecer App Store?" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Réinitialiser l'App Store ?" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Reset App Store?" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Reimpostare App Store?" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "App Storeをリセットしますか?" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "App Store를 재설정 하시겠습니까?" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zresetować App Store?" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Redefinir App Store?" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Reiniciar App Store?" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Сбросить App Store?" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Obnoviť App Store?" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ponastaviti App Store?" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "App Store'u sıfırla?" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Скинути App Store?" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đặt lại App Store?" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "重置 App Store?" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "重置 App Store?" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "重置 App Store?" } } } }, "Reset Background Task Management database" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Datenbank für Hintergrundaufgabenverwaltung zurücksetzen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Restablecer la base de datos de gestión de tareas en segundo plano" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Réinitialiser la base de données de gestion des tâches en arrière-plan" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Atur Ulang basis data Manajemen Tugas Latar Belakang" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Reimposta il database di gestione delle attività in background" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "バックグラウンドタスク管理データベースをリセット" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "백그라운드 작업 관리 데이터베이스 재설정" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zresetuj bazę danych Zarządzania Zadaniami w Tle" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Redefinir banco de dados de Gerenciamento de Tarefas em Segundo Plano" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Reiniciar a base de dados de Gestão de Tarefas em Segundo Plano" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Сбросить базу данных управления фоновыми задачами" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Obnoviť databázu Správy úloh na pozadí" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ponastavi bazo podatkov za upravljanje opravil v ozadju" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Arka Plan Görev Yönetimi veritabanını sıfırla" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Скинути базу даних управління фоновими завданнями" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đặt lại cơ sở dữ liệu Quản lý Nhiệm vụ Nền" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "重置后台任务管理数据库" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "重置背景任務管理資料庫" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "重置背景任務管理數據庫" } } } }, "Reset Settings" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Einstellungen zurücksetzten" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Restablecer configuración" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Réinitialiser les paramètres" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Setel Ulang Pengaturan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Reimposta impostazioni" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "設定をリセット" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "설정 재설정" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Przywróć ustawienia" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Redefinir Configurações" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Repor Definições" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Сбросить настройки" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Obnoviť nastavenia" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ponastavi nastavitve" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ayarları Sıfırla" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Скинути Налаштування" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thiết lập lại cài đặt" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "重置设置" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "重新設定" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "重新設定" } } } }, "Reset Size" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Grösse zurücksetzten" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Restablecer tamaño" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Réinitialiser la taille" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Atur Ulang Ukuran" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Reimposta Dimensione" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "サイズをリセット" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "크기 재설정" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Przywróć rozmiar" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Redefinir Tamanho" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Redefinir Tamanho" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Сбросить размер" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Obnoviť veľkosť" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ponastavi velikost" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Boyutu Sıfırla" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Скинути Розмір" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thiết lập lại kích thước" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "重置大小" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "重設大小" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "重設大小" } } } }, "Resetting BTM database..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "BTM-Datenbank wird zurückgesetzt..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Restableciendo la base de datos BTM..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Réinitialisation de la base de données BTM..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Mengatur ulang database BTM..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Reimpostazione del database BTM..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "BTMデータベースをリセットしています..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "BTM 데이터베이스 재설정 중..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Resetowanie bazy danych BTM..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Redefinindo o banco de dados BTM..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A redefinir a base de dados BTM..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Сброс базы данных BTM..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Obnovuje sa databáza BTM..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ponastavitev baze podatkov BTM..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "BTM veritabanı sıfırlanıyor..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Скидання бази даних BTM..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang đặt lại cơ sở dữ liệu BTM..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "重置BTM数据库..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "重置BTM資料庫..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "重置BTM資料庫..." } } } }, "Restart" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Neustart" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Reiniciar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Redémarrer" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Mulai Ulang" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Riavvia" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "再起動" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "재시작" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Uruchom ponownie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Reiniciar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Reiniciar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Перезапустить" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Reštartovať" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ponovni zagon" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yeniden başlat" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Перезапуск" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Khởi động lại" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "重新启动" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "重新啟動" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "重新啟動" } } } }, "Restart %@ for changes to take effect" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Starten Sie %@ neu, damit die Änderungen wirksam werden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Reinicia %@ para que los cambios surtan efecto" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Redémarrez %@ pour que les modifications prennent effet" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Mulai ulang %@ agar perubahan diterapkan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Riavvia %@ per applicare le modifiche" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "変更を有効にするために%@を再起動してください" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "변경 사항을 적용하려면 %@을(를) 다시 시작하세요" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Uruchom ponownie %@, aby zmiany zaczęły obowiązywać" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Reinicie %@ para que as alterações tenham efeito" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Reinicie %@ para que as alterações tenham efeito" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Перезапустите %@, чтобы изменения вступили в силу" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Reštartuj %@ aby sa zmeny prejavili" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Znova zaženite %@, da spremembe začnejo veljati" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Değişiklerin etkili olması için %@’ı yeniden başlat" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Перезапустити %@ щоб зміни вступили в дію" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Sau khi cấp quyền cần khởi động lại %@ để có hiệu lực" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "重新启动 %@ 以使变更生效" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "重新啟動 %@ 來使變更生效" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "重新啟動 %@ 來使變更生效" } } } }, "Restart the service" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dienst neu starten" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Reiniciar el servicio" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Redémarrer le service" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Mulai ulang layanan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Riavvia il servizio" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "サービスを再起動" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "서비스 재시작" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Uruchom ponownie serwis" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Reiniciar o serviço" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Reiniciar o serviço" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Перезапустите службу" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Reštartovať službu" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Znova zaženi storitev" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hizmeti yeniden başlat" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Перезапустити службу" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Khởi động lại dịch vụ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "重启服务" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "重新啟動服務" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "重新啟動服務" } } } }, "Restore %lld" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Wiederherstellen %lld" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Restaurar %lld" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Restaurer %lld" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pulihkan %lld" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ripristina %lld" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "復元 %lld" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%lld 복원" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Przywróć %lld" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Restaurar %lld" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Restaurar %lld" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Восстановить %lld" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Obnoviť %lld" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Obnovi %lld" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Geri Yükle %lld" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Відновити %lld" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Khôi phục %lld" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "恢复 %lld" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "恢復 %lld" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "恢復 %lld" } } } }, "Restore previously deleted files from trash (last 10 operations)" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Wiederherstellen von zuvor gelöschten Dateien aus dem Papierkorb (letzte 10 Vorgänge)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Restaurar archivos eliminados previamente de la papelera (últimas 10 operaciones)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Restaurer les fichiers supprimés précédemment de la corbeille (dernières 10 opérations)" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pulihkan file yang sebelumnya dihapus dari tempat sampah (10 operasi terakhir)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ripristina i file precedentemente eliminati dal cestino (ultime 10 operazioni)" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ゴミ箱から以前に削除したファイルを復元する(直近の10回の操作)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "휴지통에서 이전에 삭제한 파일 복원 (최근 10개 작업)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Przywróć wcześniej usunięte pliki z kosza (ostatnie 10 operacji)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Restaurar arquivos excluídos anteriormente da lixeira (últimas 10 operações)" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Restaurar ficheiros previamente eliminados da lixeira (últimas 10 operações)" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Восстановить ранее удаленные файлы из корзины (последние 10 операций)" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Obnoviť predtým odstránené súbory z koša (posledných 10 operácií)" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Obnovi prej izbrisane datoteke iz koša (zadnjih 10 operacij)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Çöp kutusundan daha önce silinmiş dosyaları geri yükle (son 10 işlem)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Відновити раніше видалені файли з кошика (останні 10 операцій)" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Khôi phục các tệp đã xóa trước đây từ thùng rác (10 thao tác gần nhất)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "从回收站恢复先前删除的文件(最近10次操作)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "從垃圾桶中恢復先前刪除的檔案(最近 10 次操作)" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "從垃圾桶中恢復先前刪除的文件(最近 10 次操作)" } } } }, "Restoring..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Wiederherstellen..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Restaurando..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Restauration..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Memulihkan..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ripristino in corso..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "復元中..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "복원 중..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Przywracanie..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Restaurando..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A restaurar..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Восстановление..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Obnovovanie..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Obnavljanje..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Geri yükleniyor..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Відновлення..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang khôi phục..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "正在恢复..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "正在恢復..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "正在恢復..." } } } }, "Retry" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Erneut versuchen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Reintentar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Réessayer" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Coba Lagi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Riprova" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "再試行" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "재시도" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ponów próbę" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Tentar Novamente" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Tentar novamente" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Повторить" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Znova" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ponovno poskusi" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Tekrar dene" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Повторити" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thử lại" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "重试" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "重試" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "重試" } } } }, "Retry permission check" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Berechtigungsprüfung wiederholen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Reintentar verificación de permisos" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Réessayer la vérification des autorisations" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Coba ulang pemeriksaan izin" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Riprova il controllo delle autorizzazioni" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "権限チェックを再試行" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "권한 확인 재시도" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ponów sprawdzanie uprawnień" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Tentar novamente a verificação de permissão" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Repetir verificação de permissões" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Повторить проверку разрешений" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Znova skontrolovať povolenia" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ponovno preveri dovoljenja" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İzin kontrolünü yeniden dene" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Повторити перевірку дозволів" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thử lại kiểm tra quyền" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "重试权限检查" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "重試權限檢查" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "重試權限檢查" } } } }, "Reverse search completed successfully" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Rückwärtssuche erfolgreich abgeschlossen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "La búsqueda inversa se completó con éxito" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Recherche inversée terminée avec succès" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pencarian balik selesai dengan sukses" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ricerca inversa completata con successo" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "逆検索が成功しました" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "역방향 검색이 성공적으로 완료되었습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyszukiwanie odwrotne zakończone pomyślnie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Pesquisa reversa concluída com sucesso" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Pesquisa reversa concluída com sucesso" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Обратный поиск успешно завершен" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Reverzné vyhľadávanie úspešne dokončené" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Obratno iskanje uspešno zaključeno" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ters arama başarıyla tamamlandı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Зворотній пошук успішно завершено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đã hoàn tất tìm kiếm ngược" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "反向搜索成功完成" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "已成功完成反向搜尋" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "已成功完成反向搜尋" } } } }, "Revert to original schedule" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Zum ursprünglichen Zeitplan zurückkehren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Volver al horario original" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Revenir à l'horaire initial" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Kembali ke jadwal semula" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Torna al programma originale" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "元のスケジュールに戻す" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "원래 일정으로 되돌리기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Przywróć pierwotny harmonogram" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Reverter para o cronograma original" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Reverter para o agendamento original" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Вернуться к первоначальному расписанию" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vrátiť sa k pôvodnému rozvrhu" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Vrni na prvotni urnik" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Orijinal programa geri dön" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Повернутися до початкового розкладу" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Quay lại lịch trình ban đầu" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "恢复到原始计划" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "恢復到原始計劃" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "恢復到原本時間表" } } } }, "Right click to reset size" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Rechtsklick zum Zurücksetzen der Grösse" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Haga clic derecho para restablecer el tamaño" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Cliquer droit pour réinitialiser la taille" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Klik kanan untuk mengatur ulang ukuran" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Fare clic con il tasto destro per reimpostare la dimensione" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "右クリックでサイズをリセット" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "크기 재설정을 위해 오른쪽 클릭" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Kliknij prawym przyciskiem myszy, aby przywrócić rozmiar" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Clique com o botão direito para redefinir o tamanho" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Clique com o botão direito para redefinir o tamanho" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Щелкните правой кнопкой мыши, чтобы сбросить размер" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pravým kliknutím obnoviť veľkosť" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Z desnim klikom ponastavite velikost" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Boyutu sıfırlamak için sağ tıkla" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Правий клік для скидання розміру" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Nhấn chuột phải để thiết lập lại kích thước" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "右键单击以重置大小" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "點按右鍵來重設大小" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "點按右鍵來重設大小" } } } }, "Run Cleanup" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Bereinigung ausführen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ejecutar limpieza" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Exécuter le nettoyage" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Jalankan Pembersihan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Esegui Pulizia" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "クリーンアップを実行" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "정리 실행" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Uruchom czyszczenie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Executar Limpeza" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Executar Limpeza" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Запустить очистку" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Spustiť čistenie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Zaženi čiščenje" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Temizliği Çalıştır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Запустити очищення" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chạy Dọn Dẹp" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "运行清理" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "執行清理" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "執行清理" } } } }, "Run Command:" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Befehl ausführen:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ejecutar comando:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Exécuter la commande :" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Jalankan Perintah:" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Esegui comando:" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "コマンドを実行:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "명령 실행:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Uruchom polecenie:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Executar Comando:" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Executar Comando:" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Выполнить команду:" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Spustiť príkaz:" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Zaženi ukaz:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Komutu Çalıştır:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Виконати команду:" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chạy lệnh:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "运行命令:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "執行命令:" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "執行命令:" } } } }, "Run Doctor" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Doktor ausführen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ejecutar Doctor" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Exécuter Docteur" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Jalankan Dokter" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Esegui Doctor" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ドクターを実行" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "검사기 실행" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Uruchom Doktora" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Executar Doutor" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Executar Doctor" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Запустить Доктор" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Spustiť Doktora" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Zaženi Doctor" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Doktoru Çalıştır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Запустити Доктор" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chạy Bác sĩ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "运行医生" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "運行醫生" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "運行醫生" } } } }, "Run Type: %@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Lauftyp: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Tipo de ejecución: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Type d'exécution : %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Jenis Lari: %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Tipo di esecuzione: %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "実行タイプ: %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "실행 유형: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Typ uruchomienia: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Tipo de Execução: %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Tipo de Execução: %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Тип запуска: %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Typ behu: %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Vrsta zagona: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Çalıştırma Türü: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Тип запуску: %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Loại chạy: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "运行类型:%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "運行類型:%@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "運行類型:%@" } } } }, "Running homebrew cleanup" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew-Bereinigung wird ausgeführt" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ejecutando limpieza de homebrew" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Exécution du nettoyage de homebrew" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Menjalankan pembersihan homebrew" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Esecuzione pulizia homebrew" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ホームブルーのクリーンアップを実行中" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 정리 실행 중" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Uruchamianie czyszczenia homebrew" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Executando limpeza do homebrew" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Executando limpeza do homebrew" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Запуск очистки homebrew" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Spúšťa sa čistenie homebrew" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izvajanje čiščenja Homebrew" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew temizliği çalışıyor" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Виконується очищення homebrew" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang chạy dọn dẹp homebrew" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "正在运行Homebrew清理" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "正在運行Homebrew清理" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "正在運行Homebrew清理" } } } }, "Running..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Läuft..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ejecutando..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "En cours..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Sedang berjalan..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "In esecuzione..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "実行中..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "실행 중..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Uruchamianie..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Executando..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A executar..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Запуск..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Prebieha..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Se izvaja..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Çalışıyor..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Запуск..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang chạy..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "运行中..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "運行中..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "運行中..." } } } }, "Runtime:" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Laufzeit:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Tiempo de ejecución:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Durée d'exécution:" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Waktu Berjalan:" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Tempo di esecuzione:" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "実行時間:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "실행 시간:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Czas wykonania:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Tempo de execução:" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Tempo de execução:" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Время выполнения:" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Čas spustenia:" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Čas izvajanja:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Çalışma Süresi:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Час виконання:" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thời gian chạy:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "运行时间:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "運行時間:" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "運行時間:" } } } }, "Save" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Sichern" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Guardar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Sauvegarder" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Simpan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Salva" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "保存" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "저장" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zapisz" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Salvar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Guardar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Сохранить" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Uložiť" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Shrani" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kaydet" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Зберегти" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Lưu" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "保存" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "儲存" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "儲存" } } } }, "Save this schedule" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Diesen Zeitplan speichern" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Guardar este horario" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Enregistrer ce programme" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Simpan jadwal ini" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Salva questo programma" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "このスケジュールを保存" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 일정 저장" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zapisz ten harmonogram" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Salvar este cronograma" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Salvar este cronograma" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Сохранить это расписание" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Uložiť tento rozvrh" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Shrani ta urnik" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu programı kaydet" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Зберегти цей розклад" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Lưu lịch trình này" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "保存此日程" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "保存此行程" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "儲存此時間表" } } } }, "Scan" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Scannen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Escanear" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Scruter" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pindai" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Scansiona" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "スキャン" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "스캔" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Skanuj" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Verificar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Digitalizar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Сканировать" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Skenovať" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Skeniraj" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Tara" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Сканувати" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Quét" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "扫描" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "掃描" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "掃描" } } } }, "Scan for app updates" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Nach App-Updates suchen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Buscar actualizaciones de aplicaciones" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Rechercher les mises à jour de l'application" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pindai pembaruan aplikasi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Scansiona per aggiornamenti app" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アプリの更新をスキャン" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "앱 업데이트 검색" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Skanuj aktualizacje aplikacji" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Verificar atualizações de aplicativos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Procurar atualizações de aplicativos" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Сканировать обновления приложений" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Skontrolovať aktualizácie aplikácie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Skeniraj za posodobitve aplikacij" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uygulama güncellemelerini tara" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Сканувати оновлення додатків" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Quét để cập nhật ứng dụng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "扫描应用更新" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "掃描應用程式更新" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "掃描應用程式更新" } } } }, "Scan Packages" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Pakete scannen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Escanear Paquetes" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Scanner les colis" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pindai Paket" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Scansiona Pacchetti" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "パッケージをスキャン" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "패키지 검색" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Skanuj paczki" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Escanear Pacotes" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Verificar Pacotes" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Сканировать посылки" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Skenovať balíky" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Skeniraj pakete" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Paketleri Tara" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Сканувати пакунки" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Quét Gói" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "扫描包裹" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "掃描包裹" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "掃描包裹" } } } }, "Scanning..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Scannen..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Escaneando..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Balayage…" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Memindai..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Scansione..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "スキャン中…" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "검색 중..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Skanowanie..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Verificando..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A digitalizar..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Сканирование..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Skenovanie..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Skeniranje..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Taranıyor..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Сканування..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Quét..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "扫描中..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "正在掃描..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "掃描中…" } } } }, "Schedule" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Zeitplan" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Horario" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Emploi du temps" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Jadwal" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Programma" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "スケジュール" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "일정" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Harmonogram" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Agenda" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Agendar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Расписание" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Harmonogram" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Urnik" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Program" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Розклад" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Lịch trình" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "时间表" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "時間表" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "時間表" } } } }, "Schedule is disabled" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Zeitplan ist deaktiviert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "El horario está deshabilitado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Le calendrier est désactivé" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Jadwal dinonaktifkan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "La pianificazione è disabilitata" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "スケジュールは無効です" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "일정이 비활성화되었습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Harmonogram jest wyłączony" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "A programação está desativada" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Agenda está desativada" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Расписание отключено" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Plánovanie je zakázané" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Razpored je onemogočen" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Program devre dışı bırakıldı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Розклад вимкнено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Lịch trình bị vô hiệu hóa" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "计划已禁用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "計劃已禁用" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "計劃已禁用" } } } }, "Schedule is enabled" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Zeitplan ist aktiviert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "El horario está habilitado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Le programme est activé" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Jadwal diaktifkan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "La pianificazione è abilitata" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "スケジュールが有効です" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "일정이 활성화되었습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Harmonogram jest włączony" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "A programação está ativada" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Agendamento está ativado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Расписание включено" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Harmonogram je povolený" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Razpored je omogočen" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Zamanlama etkinleştirildi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Розклад увімкнено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Lịch trình đã được kích hoạt" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "计划已启用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "計劃已啟用" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "計劃已啟用" } } } }, "Scrollbar is hidden in lists" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Rollbalken ist in Listen ausgeblendet" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "La barra de desplazamiento está oculta en las listas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "La barre de défilement est masquée dans les listes" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Bilah gulir disembunyikan dalam daftar" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "La barra di scorrimento è nascosta nelle liste" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "スクロールバーがリストに隠れる" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "목록에서 스크롤바 숨김" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pasek przewijania jest ukryty na listach" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Barra de rolagem está oculta nas listas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Barra de rolagem está oculta nas listas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Полоса прокрутки скрыта в списках" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Rolovacia lišta je skrytá v zoznamoch" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Drsni trak je skrit v seznamih" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kaydırma çubuğu listelerde gizlensin" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Смуга прокрутки прихована у списках" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ẩn thanh cuộn trong danh sách ứng dụng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "滚动条在列表中隐藏" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "捲軸在列表中隱藏" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "捲軸在列表中隱藏" } } } }, "Scrollbar is set to OS preference in lists" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Rollbalken ist in Listen ist auf die OS-Einstellung eingestellt" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "La barra de desplazamiento está configurada según la preferencia del sistema operativo en las listas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "La barre de défilement est définie selon les préférences du système d'exploitation dans les listes" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Bilah gulir diatur sesuai preferensi OS dalam daftar" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "La barra di scorrimento è impostata sulla preferenza del sistema operativo nelle liste" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "リストでスクロールバーがOS優先に設定される" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "목록 스크롤바가 OS 기본 설정으로 설정됨" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pasek przewijania jest ustawiony zgodnie z ustawieniami systemu na listach" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "A barra de rolagem é ajustada conforme a preferência do sistema operacional em listas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A barra de rolagem está definida para a preferência do sistema operativo nas listas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Полоса прокрутки настроена в соответствии с параметрами ОС в списках" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Rolovacia lišta v zoznamoch je nastavená podľa OS" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Drsnik je nastavljen na sistemske nastavitve v seznamih" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kaydırma çubuğu listelerde OS tercihine ayarlansın" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Смуга прокрутки у списках налаштована відповідно до уподобань операційної системи" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hiện thanh cuộn trong danh sách ứng dụng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "滚动条在列表中设置为操作系统偏好设置" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "捲軸在列表中設定為操作系統偏好" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "捲軸在列表中設定為操作系統偏好" } } } }, "Search" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Suchen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Buscar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Rechercher" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Cari" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Cerca" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "検索" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "검색" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Szukaj" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Pesquisar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Pesquisar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Поиск" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Hľadať" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Iskanje" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ara" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Пошук" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tìm kiếm" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "搜索" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "搜尋" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "搜尋" } } } }, "Search file content" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dateiinhalte durchsuchen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Buscar contenido del archivo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Rechercher dans le contenu des fichiers" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Cari konten file" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Cerca contenuto del file" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ファイル内容を検索" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "파일 내용 검색" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Szukaj zawartości pliku" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Pesquisar conteúdo do arquivo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Procurar conteúdo do ficheiro" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Поиск содержимого файла" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Hľadať obsah súboru" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Išči vsebino datoteke" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Dosya içeriğini ara" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Шукати вміст файлу" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tìm kiếm nội dung tệp" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "搜索文件内容" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "搜尋檔案內容" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "搜尋檔案內容" } } } }, "Search for files and folders with advanced filters" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dateien und Ordner mit erweiterten Filtern suchen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Buscar archivos y carpetas con filtros avanzados" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Rechercher des fichiers et des dossiers avec des filtres avancés" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Cari file dan folder dengan filter lanjutan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Cerca file e cartelle con filtri avanzati" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "高度なフィルターを使用してファイルとフォルダーを検索" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "고급 필터로 파일 및 폴더 검색" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyszukaj pliki i foldery za pomocą zaawansowanych filtrów" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Pesquisar arquivos e pastas com filtros avançados" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Procurar ficheiros e pastas com filtros avançados" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Поиск файлов и папок с расширенными фильтрами" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vyhľadávanie súborov a priečinkov s pokročilými filtrami" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Iskanje datotek in map z naprednimi filtri" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Gelişmiş filtrelerle dosya ve klasörleri ara" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Пошук файлів і папок з розширеними фільтрами" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tìm kiếm tệp và thư mục với bộ lọc nâng cao" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "使用高级筛选器搜索文件和文件夹" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "使用高級篩選器搜尋檔案和資料夾" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "使用進階篩選器搜尋檔案和資料夾" } } } }, "Search for related files" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Nach verwandten Dateien suchen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Buscar archivos relacionados" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Rechercher des fichiers associés" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Cari file terkait" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Cerca file correlati" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "関連ファイルを検索" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "관련 파일 검색" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyszukaj powiązane pliki" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Procurar arquivos relacionados" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Procurar ficheiros relacionados" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Поиск связанных файлов" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vyhľadať súvisiace súbory" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Išči povezane datoteke" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İlgili dosyaları ara" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Пошук пов’язаних файлів" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tìm kiếm các tệp liên quan" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "搜索相关文件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "搜尋相關檔案" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "搜尋相關檔案" } } } }, "Search Sensitivity" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Suchsensitivität" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Sensibilidad de búsqueda" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Sensibilité de la recherche" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Sensitivitas Pencarian" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Sensibilità di ricerca" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "検索感度" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "검색 민감도" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Czułość wyszukiwania" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Sensibilidade de Busca" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Sensibilidade de Pesquisa" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Чувствительность поиска" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Citlivosť vyhľadávania" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Občutljivost iskanja" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Arama Duyarlılığı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Чутливість пошуку" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Độ nhạy Tìm kiếm" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "搜索敏感度" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "搜尋靈敏度" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "搜尋靈敏度" } } } }, "Search these folders for applications" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Suche in diesen Ordnern nach Programmen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Buscar aplicaciones en estas carpetas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Rechercher des applications dans ces dossiers" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Cari aplikasi di folder ini" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Cerca applicazioni in queste cartelle" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "これらのフォルダーでアプリを検索" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 폴더에서 애플리케이션 검색" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Przeszukaj te foldery w poszukiwaniu aplikacji" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Pesquisar aplicativos nestas pastas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Pesquisar nestas pastas por aplicações" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Искать эти папки в приложениях" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vyhľadať aplikácie v týchto priečinkoch" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Išči te mape za aplikacije" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uygulamalar için bu dosyalar aransın" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Шукайте програми в цих папках" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tìm ứng dụng trong các thư mục này" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "在这些文件夹中搜索应用程序" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "為應用程式搜尋這些資料夾" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "為應用程式搜尋這些資料夾" } } } }, "Search..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Suchen..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Buscar..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Rechercher..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Cari..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Cerca..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "検索..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "검색..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyszukaj…" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Pesquisar..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Pesquisar..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Поиск..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Hľadať..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Iskanje..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ara..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Пошук..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tìm kiếm..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "搜索..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "搜尋..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "搜尋..." } } } }, "Searching for matching casks..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Suche nach passenden Fässern..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Buscando barriles coincidentes..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Recherche de fûts correspondants..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Mencari tong yang cocok..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ricerca di botti corrispondenti..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "一致する樽を検索しています..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "일치하는 캐스크를 검색 중..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyszukiwanie pasujących beczek..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Procurando por barris correspondentes..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A procurar barris correspondentes..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Поиск подходящих бочек..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Hľadanie zodpovedajúcich sudov..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Iskanje ustreznih sodov..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Eşleşen fıçıları arıyor..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Пошук відповідних бочок..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang tìm kiếm thùng phù hợp..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "搜索匹配的桶..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "搜尋匹配的桶..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "搜尋匹配的桶..." } } } }, "Searching for related files..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Suche nach verwandten Dateien..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Buscando archivos relacionados..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Recherche de fichiers associés..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Mencari file terkait..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ricerca di file correlati in corso..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "関連ファイルを検索しています..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "관련 파일 검색 중..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyszukiwanie powiązanych plików..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Procurando por arquivos relacionados..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A procurar ficheiros relacionados..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Поиск связанных файлов..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Hľadanie súvisiacich súborov..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Iskanje povezanih datotek..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İlgili dosyalar aranıyor..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Пошук пов'язаних файлів..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang tìm kiếm các tệp liên quan..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "正在搜索相关文件..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "正在搜尋相關檔案..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "正在搜尋相關檔案..." } } } }, "Searching:" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Suchen:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Buscando:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Recherche en cours…" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Sedang mencari:" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ricerca in corso:" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "検索中:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "검색 중:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyszukiwanie:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Pesquisando:" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Pesquisando:" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Поиск:" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Hľadá sa:" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Iskanje:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Aranıyor:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Пошук:" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang tìm kiếm:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "搜索中..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "正在搜尋:" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "正在搜尋:" } } } }, "Secure" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Sicher" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Seguro" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Sécurisé" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Aman" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Sicuro" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "安全" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "보안" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Bezpieczny" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Seguro" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Seguro" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Безопасный" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Bezpečný" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Varno" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Güvenli" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Безпечний" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Bảo mật" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "安全" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "安全" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "安全" } } } }, "See app details" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "App-Details anzeigen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ver detalles de la aplicación" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Détails de l’application" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lihat detail aplikasi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Vedi dettagli app" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アプリの詳細を見る" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "앱 세부정보 보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zobacz szczegóły aplikacji" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ver detalhes do aplicativo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ver detalhes do aplicativo" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Посмотреть подробности о приложении" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť podrobnosti aplikácie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Poglej podrobnosti aplikacije" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uygulama detaylarını gör" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Переглянути відомості про програму" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xem chi tiết ứng dụng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "查看应用详情" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "查看應用程式詳細信息" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "查看應用程式詳情" } } } }, "See details" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Details anzeigen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ver detalles" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Voir les détails" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lihat detail" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Vedi dettagli" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "詳細を表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "자세히 보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zobacz szczegóły" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ver detalhes" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ver detalhes" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Посмотреть подробности" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť podrobnosti" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Poglej podrobnosti" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ayrıntıları gör" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Переглянути деталі" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xem chi tiết" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "查看详情" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "查看詳情" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "查看詳情" } } } }, "See less" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Weniger sehen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ver menos" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Voir moins" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lihat lebih sedikit" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Vedi meno" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "表示を減らす" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "간단히 보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zobacz mniej" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ver menos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ver menos" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Смотреть меньше" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť menej" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pokaži manj" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Daha az gör" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Показати менше" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xem ít hơn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "显示较少" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "顯示較少" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "顯示較少" } } } }, "See lipo details" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Details zur Lipo anzeigen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ver detalles de lipo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Voir les détails lipo" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lihat detail lipo" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Vedi dettagli lipo" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "lipo の詳細を表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "번들 축소 세부정보 보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zobacz szczegóły lipo" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ver detalhes do lipo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ver detalhes do lipo" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Посмотреть детали lipo" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť podrobnosti lipo balíka" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Poglej podrobnosti lipo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Lipo ayrıntılarını gör" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Переглянути деталі lipo" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xem chi tiết lipo" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "查看 lipo 详情" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "查看 lipo 詳情" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "查看 lipo 詳情" } } } }, "See more" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Mehr sehen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ver más" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Voir plus" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lihat lebih banyak" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Vedi di più" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "もっと見る" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "더 보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zobacz więcej" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ver mais" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ver mais" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Смотреть больше" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť viac" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Poglej več" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Daha fazla gör" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Дивитися більше" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xem thêm" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "查看更多" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "查看更多" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "查看更多" } } } }, "See Packages" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Pakete ansehen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ver paquetes" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Voir les forfaits" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lihat Paket" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Vedi Pacchetti" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "パッケージを見る" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "패키지 보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zobacz pakiety" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ver pacotes" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ver Pacotes" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Посмотреть пакеты" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť balíky" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Poglej pakete" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Paketleri Gör" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Переглянути пакети" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xem các gói" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "查看套餐" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "查看套餐" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "查看套餐" } } } }, "Select a Homebrew cask to manage this app. Homebrew will adopt the existing installation without moving or duplicating files." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Wählen Sie ein Homebrew-Cask, um diese App zu verwalten. Homebrew übernimmt die bestehende Installation, ohne Dateien zu verschieben oder zu duplizieren." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Seleccione un cask de Homebrew para gestionar esta aplicación. Homebrew adoptará la instalación existente sin mover ni duplicar archivos." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Sélectionnez un cask Homebrew pour gérer cette application. Homebrew adoptera l'installation existante sans déplacer ni dupliquer les fichiers." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pilih cask Homebrew untuk mengelola aplikasi ini. Homebrew akan mengadopsi instalasi yang ada tanpa memindahkan atau menduplikasi file." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Seleziona un cask di Homebrew per gestire questa app. Homebrew adotterà l'installazione esistente senza spostare o duplicare file." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "このアプリを管理するためのHomebrewのカスクを選択してください。Homebrewは既存のインストールを移動または複製することなく採用します。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 앱을 관리할 Homebrew 캐스크를 선택하세요. Homebrew는 파일을 이동하거나 복제하지 않고 기존 설치를 채택합니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wybierz cask Homebrew, aby zarządzać tą aplikacją. Homebrew przyjmie istniejącą instalację bez przenoszenia lub duplikowania plików." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Selecione um barril do Homebrew para gerenciar este aplicativo. O Homebrew adotará a instalação existente sem mover ou duplicar arquivos." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Selecione um cask do Homebrew para gerir esta aplicação. O Homebrew adotará a instalação existente sem mover ou duplicar ficheiros." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Выберите кег Homebrew для управления этим приложением. Homebrew примет существующую установку без перемещения или дублирования файлов." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vyberte si Homebrew cask na správu tejto aplikácie. Homebrew prijme existujúcu inštaláciu bez presunu alebo duplikovania súborov." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izberite Homebrew sod, da upravljate to aplikacijo. Homebrew bo prevzel obstoječo namestitev brez premikanja ali podvajanja datotek." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu uygulamayı yönetmek için bir Homebrew fıçısı seçin. Homebrew, dosyaları taşımadan veya kopyalamadan mevcut kurulumu benimseyecektir." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Виберіть Homebrew cask для управління цим додатком. Homebrew прийме існуючу установку без переміщення або дублювання файлів." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chọn một cask Homebrew để quản lý ứng dụng này. Homebrew sẽ chấp nhận cài đặt hiện có mà không di chuyển hoặc sao chép tệp." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "选择一个 Homebrew cask 来管理此应用程序。Homebrew 将在不移动或复制文件的情况下采用现有安装。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "選擇一個 Homebrew cask 來管理此應用程式。Homebrew 將在不移動或複製文件的情況下採用現有的安裝。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "選擇一個 Homebrew cask 來管理此應用程式。Homebrew 將在不移動或複製文件的情況下採用現有的安裝。" } } } }, "Select All" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Alles auswählen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Seleccionar todo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Tout sélectionner" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pilih Semua" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Seleziona tutto" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "すべて選択" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "모두 선택" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zaznacz wszystko" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Selecionar tudo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Selecionar Tudo" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Выбрать все" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vybrať všetko" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izberi vse" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Tümünü Seç" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Вибрати все" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chọn tất cả" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "全选" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "全選" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "全選" } } } }, "Select all files in this category" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Alle Dateien in dieser Kategorie auswählen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Seleccionar todos los archivos en esta categoría" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Sélectionner tous les fichiers dans cette catégorie" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pilih semua file dalam kategori ini" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Seleziona tutti i file in questa categoria" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "このカテゴリのすべてのファイルを選択" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 범주의 모든 파일 선택" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wybierz wszystkie pliki w tej kategorii" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Selecionar todos os arquivos nesta categoria" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Selecionar todos os ficheiros nesta categoria" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Выбрать все файлы в этой категории" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vybrať všetky súbory v tejto kategórii" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izberi vse datoteke v tej kategoriji" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu kategorideki tüm dosyaları seç" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Вибрати всі файли в цій категорії" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chọn tất cả các tệp trong danh mục này" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "选择此类别中的所有文件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "選擇此類別中的所有文件" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "選擇此類別中的所有文件" } } } }, "Select an app from the sidebar to begin" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Wählen Sie eine App aus der Seitenleiste, um zu beginnen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Seleccione una aplicación de la barra lateral para comenzar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Sélectionnez une application dans la barre latérale pour commencer" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pilih aplikasi dari bilah samping untuk memulai" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Seleziona un'app dalla barra laterale per iniziare" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "サイドバーからアプリを選択して開始してください" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "시작하려면 사이드바에서 앱을 선택하세요" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wybierz aplikację z paska bocznego, aby rozpocząć" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Selecione um aplicativo na barra lateral para começar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Selecione uma aplicação na barra lateral para começar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Выберите приложение на боковой панели, чтобы начать" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vyberte aplikáciu z bočného panela na začiatok" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izberite aplikacijo iz stranske vrstice za začetek" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Başlamak için kenar çubuğundan bir uygulama seçin" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Виберіть програму з бічної панелі, щоб розпочати" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chọn một ứng dụng từ thanh bên để bắt đầu" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "从侧边栏中选择一个应用以开始" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "從側邊欄選擇一個應用程式以開始" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "從側邊欄中選擇一個應用程序以開始" } } } }, "Select an app to view details" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Wählen Sie eine App aus, um Details anzuzeigen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Seleccione una aplicación para ver los detalles" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Sélectionnez une application pour voir les détails" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pilih aplikasi untuk melihat detailnya" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Seleziona un'app per visualizzare i dettagli" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "詳細を表示するアプリを選択" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "앱을 선택하여 세부정보를 확인하세요" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wybierz aplikację, aby zobaczyć szczegóły" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Selecione um aplicativo para ver detalhes" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Selecione uma aplicação para ver os detalhes" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Выберите приложение для просмотра деталей" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vyberte aplikáciu na zobrazenie podrobností" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izberite aplikacijo za ogled podrobnosti" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Detayları görüntülemek için bir uygulama seçin" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Виберіть додаток, щоб переглянути деталі" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chọn một ứng dụng để xem chi tiết" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "选择一个应用以查看详细信息" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "選擇應用程式以查看詳情" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "選擇應用程式以查看詳情" } } } }, "Select an environment to view stored cache" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Wählen Sie eine Umgebung aus, um den gespeicherten Cache anzuzeigen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Seleccione un entorno para ver la caché almacenada" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Sélectionnez un environnement pour afficher le cache stocké" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pilih lingkungan untuk melihat tembolok yang disimpan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Seleziona un ambiente per visualizzare la cache memorizzata" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "キャッシュを表示する環境を選択してください" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "저장된 캐시를 보려면 환경을 선택하십시오" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wybierz środowisko, aby wyświetlić zapisaną pamięć podręczną" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Selecione um ambiente para ver o cache armazenado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Selecione um ambiente para ver a cache armazenada" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Выберите среду для просмотра сохраненного кеша" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vyberte prostredie na zobrazenie uloženého vyrovnávacej pamäte" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izberite okolje za ogled shranjenega predpomnilnika" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Önbelleğe alınan verileri görüntülemek için bir ortam seçin" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Виберіть середовище для перегляду збереженого кешу" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chọn một môi trường để xem bộ đệm được lưu trữ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "选择一个环境查看已存储的缓存" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "選擇一個環境以查看已儲存的緩存" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "選擇環境以查看已儲存的緩存" } } } }, "Selected Items:" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ausgewählte Elemente:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Elementos seleccionados:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Eléments sélectionnés" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Item yang Dipilih:" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Elementi selezionati:" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "選択された項目:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "선택된 항목:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wybrane rzeczy:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Itens Selecionados:" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Itens Selecionados:" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Выбранные элементы:" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vybrané položky:" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Izbrani elementi:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Seçilen Ögeler:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Вибрані елементи:" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Các mục đã chọn:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "选择的项目:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "選擇的項目:" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "所選項目:" } } } }, "Sentinel Monitor" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Sentinel-Monitor" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Monitor Centinela" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Moniteur Sentinel" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pemantau Sentinel" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Monitor Sentinella" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Sentinelモニター" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Sentinel Monitor" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Monitor Sentinel" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Monitor Sentinel" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Monitor de Sentinela" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Мониторинг удалений" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Sentinel Monitor" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Sentinel Monitor" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sentinel Monitör" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Сторожовий Монітор" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Trình giám sát Sentinel" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Sentinel 垃圾监视器" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Sentinel 垃圾監視器" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "Sentinel 垃圾監視器" } } } }, "Sentinel Monitor found no orphaned files" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Der Sentinel-Monitor hat keine verwaisten Dateien gefunden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "El monitor Sentinel no encontró archivos huérfanos" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Le moniteur Sentinel n'a trouvé aucun fichier orphelin" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Sentinel Monitor tidak menemukan file yatim" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Sentinel Monitor non ha trovato file orfani" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Sentinel Monitorは孤立したファイルを見つけませんでした" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Sentinel Monitor에서 연결되지 않은 파일을 찾지 못했습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Sentinel Monitor nie znalazł osieroconych plików" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "O Monitor Sentinel não encontrou arquivos órfãos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "O Sentinel Monitor não encontrou ficheiros órfãos" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Sentinel Monitor не обнаружил осиротевших файлов" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Monitor Sentinel nenašiel žiadne osirelé súbory" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Sentinel Monitor ni našel osirotelih datotek" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sentinel Monitör yetim dosya bulamadı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Sentinel Monitor не знайшов осиротілих файлів" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Sentinel Monitor không tìm thấy tệp mồ côi nào" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "哨兵监控器未发现孤立文件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "哨兵監控器未發現孤立文件" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "哨兵監控器未發現孤立文件" } } } }, "Sentinel Monitor found no other files to remove" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Sentinel Monitor hat keine anderen Dateien zum Entfernen gefunden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "El Monitor de Sentinel no encontró otros archivos para eliminar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Sentinel Monitor n'a trouvé aucun autre fichier à supprimer\n" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Sentinel Monitor tidak menemukan file lain untuk dihapus" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Sentinel Monitor non ha trovato altri file da rimuovere" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Sentinelモニターは他のファイルを見つけられませんでした" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Sentinel Monitor found no other files to remove" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Monitor Sentinel nie znalazł żadnych innych plików do usunięcia" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "O Monitor Sentinel não encontrou outros arquivos para remover" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "O Monitor Sentinel não encontrou outros arquivos para remover" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Мониторинг удалений не обнаружил других файлов для удаления" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Sentinel Monitor nenašiel žiadne ďalšie súbory na odstránenie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Sentinel Monitor ni našel drugih datotek za odstranitev" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sentinel Monitör kaldırılacak başka dosya bulamadı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Сторожовий Монітор не знайшов інших файлів для видалення" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Trình giám sát Sentinel không tìm thấy tệp nào khác để xóa" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Sentinel 垃圾监视器未找到其他要移除的文件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Sentinel 垃圾監視器找不到其他要移除的檔案" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "Sentinel 垃圾監視器找不到其他要移除的檔案" } } } }, "Service desynced, attempting recovery..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dienst nicht synchronisiert, versuche Wiederherstellung..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Servicio desincronizado, intentando recuperación..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Service désynchronisé, tentative de récupération..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Layanan tidak sinkron, mencoba pemulihan..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Servizio desincronizzato, tentativo di recupero..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "サービスが同期していません。復旧を試みています..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "서비스가 비동기화되었습니다. 복구 시도 중..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usługa rozsynchronicowana, próba odzyskiwania..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Serviço dessincronizado, tentando recuperação..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Serviço dessincronizado, a tentar recuperação..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Служба рассинхронизирована, пытаемся восстановить..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Služba je nesynchronizovaná, pokúšam sa o obnovu..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Storitev ni sinhronizirana, poskus obnavljanja..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hizmet senkronize edilmedi, kurtarma deneniyor..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Служба десинхронізована, намагаємося відновити..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Dịch vụ không đồng bộ, đang cố gắng khôi phục..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "服务不同步,正在尝试恢复..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "服務不同步,正在嘗試恢復..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "服務不同步,正在嘗試恢復..." } } } }, "Service hasn't been registered. You may register it now." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Der Dienst wurde nicht registriert. Sie können ihn jetzt registrieren." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "El servicio no ha sido registrado. Puede registrarlo ahora." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Le service n'a pas été enregistré. Vous pouvez l'enregistrer maintenant." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Layanan belum terdaftar. Anda dapat mendaftarkannya sekarang." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Il servizio non è stato registrato. Puoi registrarlo ora." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "サービスが登録されていません。今すぐ登録できます。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "서비스가 등록되지 않았습니다. 지금 등록할 수 있습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usługa nie została zarejestrowana. Możesz ją teraz zarejestrować." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "O serviço não foi registrado. Você pode registrá-lo agora." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "O serviço não foi registado. Pode registá-lo agora." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Сервис не зарегистрирован. Вы можете зарегистрировать его сейчас." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Služba nebola zaregistrovaná. Môžete ju teraz zaregistrovať." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Storitev ni bila registrirana. Zdaj jo lahko registrirate." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hizmet kaydedilmemiş. Şimdi kaydedebilirsiniz." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Сервіс не зареєстровано. Ви можете зареєструвати його зараз." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Dịch vụ chưa được đăng ký. Bạn có thể đăng ký ngay bây giờ." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "服务尚未注册。您现在可以注册。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "服務尚未註冊。您可以立即註冊。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "服務尚未註冊。您現在可以註冊。" } } } }, "Service is already enabled." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dienst ist bereits aktiviert." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "El servicio ya está habilitado." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Service déjà activé." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Layanan sudah diaktifkan." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Il servizio è già abilitato." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "サービスはすでに有効です" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "서비스가 이미 활성화되었습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usługa jest obecnie włączona." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "O serviço já está ativado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "O serviço já está ativado." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Служба уже включена." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Služba je už povolená" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Storitev je že omogočena." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Servis zaten etkin." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Послуга вже ввімкнена." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Dịch vụ đã được bật." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "服务已启用。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "已啟用服務。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "已啟用服務。" } } } }, "Service is already registered and enabled." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dienst ist bereits registriert und aktiviert." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "El servicio ya está registrado y habilitado." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Service déjà enregistré et activé." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Layanan sudah terdaftar dan diaktifkan." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Il servizio è già registrato e abilitato." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "サービスはすでに登録され、有効になっています。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "서비스가 이미 등록되어 활성화되어 있습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usługa jest obecnie zarejestrowana i włączona." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "O serviço já está registrado e habilitado." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "O serviço já está registado e ativado." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Служба уже зарегистрирована и включена." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Služba je už zaregistrovaná a povolená." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Storitev je že registrirana in omogočena." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Servis zaten kayıtlı ve etkin." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Служба вже зареєстрована та ввімкнена." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Dịch vụ đã được đăng ký và bật." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "服务已注册并启用。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "服務已註冊並且啟󠄁用。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "服務已註冊並且啟󠄁用。" } } } }, "Service is not installed." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dienst ist nicht installiert." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "El servicio no está instalado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Service non installé." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Layanan tidak terpasang." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Il servizio non è installato." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "サービスがインストールされていません" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "서비스가 설치되지 않았습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usługa nie jest zainstalowana." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Serviço não está instalado." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Serviço não está instalado." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Служба не установлена." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Služba nie je nainštalovaná." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Storitev ni nameščena." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Servis kurulu değil." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Службу не встановлено." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Dịch vụ chưa được cài đặt." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "服务未安装。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "服務尚未安裝。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "服務尚未安裝。" } } } }, "Service registered but requires user approval in Settings > Login Items > %@.app." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dienst registriert, erfordert jedoch die Zustimmung des Benutzers unter „Einstellungen“ > „Anmeldeobjekte“ > %@.app." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Servicio registrado pero requiere la aprobación del usuario en Configuración > Elementos de inicio de sesión > %@.app." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Service enregistré mais nécessite une autorisation dans Settings > Login Items > %@.app." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Layanan terdaftar tetapi memerlukan persetujuan pengguna di Pengaturan > Item Login > %@.app." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Servizio registrato ma richiede l'approvazione dell'utente in Impostazioni > Elementi di login > %@.app." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "サービスは登録されていますが、設定 > ログイン項目 > %@.app でユーザー承認が必要です" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "서비스가 등록되었지만 설정 > 로그인 항목 > %@.app에서 사용자의 승인이 필요합니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usługa została zarejestrowana, ale wymaga zatwierdzenia przez użytkownika w sekcji Ustawienia systemowe > Rzeczy i rozszerzenia otwierane podczas logowania > %@.app." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Serviço registrado, mas requer aprovação do usuário em Ajustes > Itens de Login > %@.app." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Serviço registado mas requer aprovação do utilizador em Definições > Itens de Início de Sessão > %@.app." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Служба зарегистрирована, но требует одобрения пользователя в Настройки > Объекты входа > %@.app." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Služba je zaregistrovaná, ale vyžaduje si schválenie používateľom v časti Nastavenia > Položky prihlásenia > %@.app." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Storitev je registrirana, vendar zahteva odobritev uporabnika v Nastavitve > Prijavni elementi > %@.app." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hizmet kaydedildi ancak Ayarlar > Oturum Açma Öğeleri > %@.app'de kullanıcı onayı gerekiyor." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Служба зареєстрована, але потребує схвалення користувача в Налаштуваннях > Елементи входу > %@.app." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Dịch vụ đã được đăng ký nhưng cần sự phê duyệt của người dùng trong Cài đặt > Mục Đăng nhập > %@.app." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "服务已注册,但需要在设置>登录项>%@.app中进行用户批准。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "服務已註冊,但需要用户在「設定」>「登入項目」>「%@.app」中批准。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "服務已註冊,但需要用户在「設定」>「登入項目」>「%@.app」中批准。" } } } }, "Service successfully registered and eligible to run." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dienst erfolgreich registriert und einsatzbereit." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Servicio registrado con éxito y elegible para ejecutarse." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Service enregistré avec succès et éligible pour fonctionner." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Layanan berhasil didaftarkan dan memenuhi syarat untuk dijalankan." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Servizio registrato con successo ed idoneo all'esecuzione." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "サービスが正常に登録され、実行可能です。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "서비스가 성공적으로 등록되었으며 실행할 수 있습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usługa została pomyślnie zarejestrowana i jest gotowa do uruchomienia." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Serviço registrado com sucesso e elegível para execução." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Serviço registado com sucesso e elegível para execução." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Сервис успешно зарегистрирован и готов к запуску." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Služba bola úspešne zaregistrovaná a je oprávnená na spustenie." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Storitev je bila uspešno registrirana in je primerna za zagon." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hizmet başarıyla kaydedildi ve çalışmaya uygun." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Служба успішно зареєстрована і готова до запуску." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Dịch vụ đã được đăng ký thành công và đủ điều kiện để chạy." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "服务已成功注册并可运行。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "服務已成功註冊並可運行。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "服務已成功註冊並可運行。" } } } }, "Services" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dienste" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Servicios" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Services" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Layanan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Servizi" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "サービス" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "서비스" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Usługi" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Serviços" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Serviços" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Службы" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Služby" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Storitve" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hizmetler" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Служби" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Dịch vụ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "服务" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "服務" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "服務列表" } } } }, "Set as startup page" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Als Startseite festlegen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Establecer como página de inicio" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Définir comme page de démarrage" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tetapkan sebagai halaman awal" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Imposta come pagina iniziale" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "スタートアップページとして設定" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "시작 페이지로 설정" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ustaw jako stronę startową" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Definir como página inicial" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Definir como página inicial" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Установить как стартовую страницу" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nastaviť ako úvodnú stránku" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Nastavi kot začetno stran" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Başlangıç sayfası olarak ayarla" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Встановити як стартову сторінку" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đặt làm trang khởi động" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "设为启动页" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "設為啟動頁" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "設為啟動頁面" } } } }, "Settings" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Einstellungen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Configuración" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Paramètres" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pengaturan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Impostazioni" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "設定" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "설정" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ustawienia" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Configurações" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Configurações" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Настройки" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nastavenia" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Nastavitve" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ayarlar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Налаштування" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Cài đặt" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "设置" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "設定" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "設定" } } } }, "Show announcements badge again" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "zeige nochmals die Ankündigungsplakette" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar la insignia de anuncios de nuevo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Afficher de nouveau le badge des annonces" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tampilkan lencana pengumuman lagi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Mostra di nuovo il badge degli annunci" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アナウンスバッジを再度表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "공지 배지를 다시 표시" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pokaż ponownie odznakę ogłoszeń" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar novamente o ícone de anúncios" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar novamente o ícone de anúncios" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Снова показать значок с объявлениями" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Znova zobraziť odznak oznámení" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ponovno pokaži oznako obvestil" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Duyurular işaretini tekrar göster" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Знову показати значок оголошень" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hiển thị lại biểu tượng thông báo" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "再次显示公告角标" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "再次顯示宣佈標記" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "再次顯示宣佈標記" } } } }, "Show apps list on startup" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Zeige Programmliste beim Start" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar lista de aplicaciones al iniciar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Afficher la liste des applications au démarrage" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tampilkan daftar aplikasi saat startup" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Mostra elenco app all'avvio" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "起動時にアプリリストを表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "시작 시 앱 목록 표시" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pokaż listę aplikacji podczas uruchamiania" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar lista de apps na inicialização" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar lista de aplicativos na inicialização" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Показывать список приложений при запуске" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť zoznam aplikácií pri spustení" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prikaži seznam aplikacij ob zagonu" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Açılışta uygulama listesini göster" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Показувати список програм під час запуску" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hiển thị danh sách ứng dụng khi khởi động" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "启动时显示应用列表" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "啟動時顯示 App 列表" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "啟動時顯示 App 列表" } } } }, "Show apps that are already up-to-date" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Apps anzeigen, die bereits auf dem neuesten Stand sind" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar aplicaciones que ya están actualizadas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Afficher les applications déjà à jour" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tampilkan aplikasi yang sudah terbaru" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Mostra le app già aggiornate" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "すでに最新のアプリを表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이미 최신 상태인 앱 표시" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pokaż aplikacje, które są już aktualne" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar aplicativos que já estão atualizados" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar aplicações que já estão atualizadas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Показать приложения, которые уже обновлены" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť aplikácie, ktoré sú už aktuálne" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prikaži aplikacije, ki so že posodobljene" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Zaten güncel olan uygulamaları göster" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Показати програми, які вже оновлені" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hiển thị ứng dụng đã cập nhật" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "显示已更新的应用程序" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "顯示已更新的應用程式" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "顯示已經是最新版本的應用程式" } } } }, "Show apps without a supported update mechanism" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Apps ohne unterstützten Aktualisierungsmechanismus anzeigen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar aplicaciones sin un mecanismo de actualización compatible" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Afficher les applications sans mécanisme de mise à jour pris en charge" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tampilkan aplikasi tanpa mekanisme pembaruan yang didukung" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Mostra le app senza un meccanismo di aggiornamento supportato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "サポートされている更新メカニズムがないアプリを表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "지원되는 업데이트 메커니즘이 없는 앱 표시" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pokaż aplikacje bez obsługiwanego mechanizmu aktualizacji" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar aplicativos sem um mecanismo de atualização suportado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar apps sem um mecanismo de atualização suportado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Показать приложения без поддерживаемого механизма обновления" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť aplikácie bez podporovaného mechanizmu aktualizácie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prikaži aplikacije brez podprtega mehanizma za posodobitve" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Desteklenen bir güncelleme mekanizması olmayan uygulamaları göster" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Показати програми без підтримуваного механізму оновлення" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hiển thị ứng dụng không có cơ chế cập nhật được hỗ trợ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "显示没有受支持更新机制的应用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "顯示沒有支援更新機制的應用程式" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "顯示沒有支援更新機制的應用程式" } } } }, "Show auto-updating apps in Homebrew" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Automatisch aktualisierende Apps in Homebrew anzeigen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar aplicaciones que se actualizan automáticamente en Homebrew" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Afficher les applications à mise à jour automatique dans Homebrew" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tampilkan aplikasi yang memperbarui otomatis di Homebrew" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Mostra le app che si aggiornano automaticamente in Homebrew" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Homebrewで自動更新アプリを表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew에서 자동 업데이트 앱 표시" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pokaż aplikacje z automatycznymi aktualizacjami w Homebrew" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar aplicativos de atualização automática no Homebrew" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar apps com atualização automática no Homebrew" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Показать автоматически обновляемые приложения в Homebrew" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť aplikácie s automatickou aktualizáciou v Homebrew" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prikaži aplikacije s samodejnim posodabljanjem v Homebrewu" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew'de otomatik güncellenen uygulamaları göster" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Показати програми з автоматичним оновленням у Homebrew" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hiển thị ứng dụng tự động cập nhật trong Homebrew" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "在 Homebrew 中显示自动更新的应用程序" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "在 Homebrew 中顯示自動更新的應用程式" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "在 Homebrew 中顯示自動更新的應用程式" } } } }, "Show file details" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dateidetails anzeigen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar detalles del archivo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Détails du fichier" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tampilkan detail file" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Mostra dettagli file" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ファイルの詳細を表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "파일 세부정보 표시" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pokaż szczegóły pliku" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar detalhes do arquivo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar detalhes do arquivo" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Показать детали файлов" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť podrobnosti o súbore" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prikaži podrobnosti datoteke" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Dosya ayrıntılarını göster" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Показати деталі файлу" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hiển thị chi tiết tệp" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "显示文件详情" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "顯示文件詳情" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "顯示文件詳情" } } } }, "Show file in Finder" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Datei im Finder anzeigen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar archivo en Finder" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Afficher le fichier dans la Finder" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tampilkan file di Finder" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Mostra file nel Finder" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Finderにファイルを表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Finder에서 파일 보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pokaż plik w Finderze" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar arquivo no Finder" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar ficheiro no Finder" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Показать файл в Finder" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť súbor vo Finderi" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pokaži datoteko v Finderju" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Dosyayı Finder'da göster" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Показати файл у Finder" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hiển thị tệp trong Finder" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "在访达中显示文件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "在 Finder 中顯示檔案" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "在 Finder 中顯示檔案" } } } }, "Show hidden updates" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Versteckte Updates anzeigen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar actualizaciones ocultas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Afficher les mises à jour masquées" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tampilkan pembaruan tersembunyi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Mostra aggiornamenti nascosti" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "非表示の更新を表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "숨겨진 업데이트 표시" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pokaż ukryte aktualizacje" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar atualizações ocultas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar atualizações ocultas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Показать скрытые обновления" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť skryté aktualizácie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pokaži skrite posodobitve" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Gizli güncellemeleri göster" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Показати приховані оновлення" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hiển thị các cập nhật ẩn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "显示隐藏的更新" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "顯示隱藏的更新" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "顯示隱藏的更新" } } } }, "Show in Finder" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Im Finder anzeigen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar en Finder" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Afficher dans le Finder" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tampilkan di Finder" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Mostra nel Finder" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Finder で表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Finder에서 보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pokaż w Finderze" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar no Finder" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar no Finder" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Показать в Finder" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť vo Finderi" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pokaži v Finderju" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Finder'da Göster" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Показати у Finder" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hiển thị trong Finder" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "在 Finder 中显示" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "在 Finder 中顯示" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "在 Finder 中顯示" } } } }, "Show multi-select" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Multi-Auswahl anzeigen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar selección múltiple" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Montrer la sélection multiple" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tampilkan multi-pilih" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Mostra selezione multipla" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "マルチセレクトを表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "다중 선택 표시" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pokaż wielokrotny wybór" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar seleção múltipla" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar seleção múltipla" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Показать мультивыбор" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť viacnásobný výber" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prikaži večkratni izbor" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Çoklu seçim göster" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Показати множинний вибір" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hiển thị chọn nhiều" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "显示多选" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "顯示多選" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "顯示多選" } } } }, "Show only formulae installed on request (not as dependencies)" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Nur Formeln anzeigen, die auf Anfrage installiert wurden (nicht als Abhängigkeiten)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar solo fórmulas instaladas a pedido (no como dependencias)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Afficher uniquement les formules installées sur demande (pas en tant que dépendances)" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tampilkan hanya formula yang diinstal berdasarkan permintaan (bukan sebagai dependensi)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Mostra solo le formule installate su richiesta (non come dipendenze)" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "リクエストに応じてインストールされた数式のみを表示(依存関係としてではなく)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "요청 시 설치된 공식만 표시 (종속성으로서가 아님)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pokaż tylko formuły zainstalowane na żądanie (nie jako zależności)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar apenas fórmulas instaladas sob demanda (não como dependências)" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar apenas fórmulas instaladas a pedido (não como dependências)" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Показывать только формулы, установленные по запросу (не как зависимости)" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť iba vzorce nainštalované na požiadanie (nie ako závislosti)" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prikaži samo formule, nameščene na zahtevo (ne kot odvisnosti)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yalnızca isteğe bağlı olarak yüklenen formülleri göster (bağımlılık olarak değil)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Показувати лише формули, встановлені за запитом (не як залежності)" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chỉ hiển thị công thức được cài đặt theo yêu cầu (không phải là phụ thuộc)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "仅显示按请求安装的公式(不作为依赖项)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "僅顯示按要求安裝的公式(不作為依賴項)" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "僅顯示按要求安裝的公式(不作為依賴項)" } } } }, "Show page" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Seite anzeigen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar página" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Afficher la page" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tampilkan halaman" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Mostra pagina" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ページを表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "페이지 보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pokaż stronę" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar página" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar página" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Показать страницу" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť stránku" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prikaži stran" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sayfayı göster" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Показати сторінку" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hiển thị trang" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "显示页面" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "顯示頁面" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "顯示頁面" } } } }, "Show sidebar on view load" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Seitenleiste beim Laden der Ansicht anzeigen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar barra lateral al cargar la vista" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Afficher la barre latérale au chargement de la vue" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tampilkan bilah samping saat memuat tampilan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Mostra la barra laterale al caricamento della vista" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ビュー読み込み時にサイドバーを表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "뷰 로드 시 사이드바 표시" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pokaż pasek boczny przy ładowaniu widoku" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar barra lateral ao carregar a visualização" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar barra lateral ao carregar a vista" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Показать боковую панель при загрузке вида" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť bočný panel pri načítaní zobrazenia" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prikaži stransko vrstico ob nalaganju pogleda" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Görünüm yüklendiğinde kenar çubuğunu göster" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Показати бічну панель при завантаженні перегляду" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hiển thị thanh bên khi tải chế độ xem" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "视图加载时显示侧边栏" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "在視圖加載時顯示側邊欄" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "視圖加載時顯示側邊欄" } } } }, "Showing last 3 releases" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Zeige die letzten 3 Versionen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Mostrando las últimas 3 versiones" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Affichage des 3 dernières versions" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Menampilkan 3 rilis terakhir" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Mostrando le ultime 3 versioni" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "最近の3つのリリースを表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "마지막 3개의 릴리스 표시" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyświetlanie ostatnich 3 wydań" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Mostrando os últimos 3 lançamentos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Mostrando as últimas 3 versões" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Показаны последние 3 выпуска" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobrazujú sa posledné 3 verzie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prikaz zadnjih 3 izdaj" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Son 3 sürüm gösteriliyor" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Показувати останні 3 релізи" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang hiển thị 3 phiên bản mới nhất" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "显示近期的 3 次更新" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "顯示最近 3 個版本" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "顯示最近 3 個版本" } } } }, "Simple app list disabled" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Einfache App-Liste deaktiviert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Lista de aplicaciones simple desactivada" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Liste simple d’application désactivée" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Daftar aplikasi sederhana dinonaktifkan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Elenco app semplice disabilitato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "シンプルなアプリリストが無効です" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "간단한 앱 목록 비활성화됨" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyłączono prosta listę aplikacji" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Lista simples de aplicativos desativada" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Lista de aplicativos simples desativada" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Отключен упрощённый список приложений" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zoznam aplikácií je vypnutý" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Preprost seznam aplikacij onemogočen" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Basit uygulama listesi devre dışı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Список простих додатків вимкнено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Danh sách ứng dụng đơn giản bị vô hiệu hoá" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "简单应用列表已禁用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "簡易應用程式列表已停用" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "簡單應用程式列表已停用" } } } }, "Simple app list enabled" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Einfache Appliste aktiviert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Lista de aplicaciones simples habilitada" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Liste simple d’application activée" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Daftar aplikasi sederhana diaktifkan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Elenco app semplice abilitato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "シンプルなアプリリストが有効になっています" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "단순 앱 목록 활성화됨" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Włączono prostą listę aplikacji" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Lista de aplicativos simples ativada" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Lista de apps simples ativada" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Включен простой список приложений" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Jednoduchý zoznam aplikácií zapnutý" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Preprost seznam aplikacij omogočen" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Basit uygulama listesi etkinleştirildi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Увімкнено спрощений список додатків" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Danh sách ứng dụng đơn giản đã được bật" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "启用简易应用列表" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "簡易應用程式列表已啟用" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "簡單應用程式列表已啟用" } } } }, "Since **AuthorizationExecuteWithPrivileges** has been deprecated by Apple as a less secure authentication method, it has been removed from Pearcleaner and the helper tool will be the only option going forward." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Da **AuthorizationExecuteWithPrivileges** von Apple als weniger sichere Authentifizierungsmethode abgelehnt wurde, wurde es aus Pearcleaner entfernt und das Hilfsprogramm wird die einzige Option in Zukunft sein." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Dado que **AuthorizationExecuteWithPrivileges** ha sido declarado obsoleto por Apple como un método de autenticación menos seguro, ha sido eliminado de Pearcleaner y la herramienta auxiliar será la única opción en el futuro." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Étant donné que **AuthorizationExecuteWithPrivileges** a été déclaré obsolète par Apple en tant que méthode d'authentification moins sécurisée, il a été supprimé de Pearcleaner et l'outil d'assistance sera la seule option à l'avenir." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Karena **AuthorizationExecuteWithPrivileges** telah dihentikan oleh Apple sebagai metode otentikasi yang kurang aman, itu telah dihapus dari Pearcleaner dan alat bantu akan menjadi satu-satunya pilihan ke depan." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Poiché **AuthorizationExecuteWithPrivileges** è stato deprecato da Apple come metodo di autenticazione meno sicuro, è stato rimosso da Pearcleaner e lo strumento di supporto sarà l'unica opzione d'ora in poi." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Apple によって **AuthorizationExecuteWithPrivileges** がより安全性の低い認証方法として廃止されたため、Pearcleaner から削除され、今後はヘルパーツールのみが選択肢となります。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Apple에 의해 **AuthorizationExecuteWithPrivileges**가 덜 안전한 인증 방법으로 사용 중단되었기 때문에 Pearcleaner에서 제거되었으며, 앞으로는 도우미 도구만 사용할 수 있습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ponieważ **AuthorizationExecuteWithPrivileges** został uznany przez Apple za mniej bezpieczną metodę uwierzytelniania, został usunięty z Pearcleaner, a narzędzie pomocnicze będzie jedyną opcją w przyszłości." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Como **AuthorizationExecuteWithPrivileges** foi descontinuado pela Apple como um método de autenticação menos seguro, ele foi removido do Pearcleaner e a ferramenta auxiliar será a única opção daqui para frente." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Como o **AuthorizationExecuteWithPrivileges** foi descontinuado pela Apple por ser um método de autenticação menos seguro, ele foi removido do Pearcleaner e a ferramenta auxiliar será a única opção daqui para frente." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Поскольку **AuthorizationExecuteWithPrivileges** был признан Apple устаревшим как менее безопасный метод аутентификации, он был удален из Pearcleaner, и в будущем будет доступен только вспомогательный инструмент." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Keďže **AuthorizationExecuteWithPrivileges** bol spoločnosťou Apple označený ako menej bezpečná metóda autentifikácie, bol odstránený z Pearcleaner a pomocný nástroj bude jedinou možnosťou do budúcna." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ker je Apple **AuthorizationExecuteWithPrivileges** označil kot manj varno metodo overjanja, je bila odstranjena iz Pearcleanerja, pomožno orodje pa bo v prihodnje edina možnost." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Apple tarafından daha az güvenli bir kimlik doğrulama yöntemi olarak kullanımdan kaldırılan **AuthorizationExecuteWithPrivileges**, Pearcleaner'dan kaldırılmıştır ve yardımcı araç gelecekte tek seçenek olacaktır." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оскільки **AuthorizationExecuteWithPrivileges** був визнаний Apple застарілим як менш безпечний метод автентифікації, його було видалено з Pearcleaner, і в майбутньому буде доступний лише допоміжний інструмент." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Vì **AuthorizationExecuteWithPrivileges** đã bị Apple không còn hỗ trợ do là phương pháp xác thực kém an toàn hơn, nó đã bị loại bỏ khỏi Pearcleaner và công cụ trợ giúp sẽ là lựa chọn duy nhất trong tương lai." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "由于 **AuthorizationExecuteWithPrivileges** 被 Apple 弃用为不太安全的身份验证方法,因此已从 Pearcleaner 中移除,未来将仅能使用辅助工具。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "由於 **AuthorizationExecuteWithPrivileges** 被 Apple 廢棄為較不安全的身份驗證方法,因此已從 Pearcleaner 中移除,未來將僅能使用輔助工具。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "由於 **AuthorizationExecuteWithPrivileges** 被 Apple 廢棄為較不安全的身份驗證方法,因此已從 Pearcleaner 中移除,未來將僅能使用輔助工具。" } } } }, "Since: %@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Seit: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Desde: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Depuis : %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Sejak: %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Da: %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "以来: %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이후: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Od: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Desde: %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Desde: %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "С: %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Od: %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Od: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Beraber: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "З: %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Kể từ: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "自:%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "自:%@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "自:%@" } } } }, "Size" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Grösse" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Tamaño" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Taille" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ukuran" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Dimensione" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "サイズ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "크기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Rozmiar" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Tamanho" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Tamanho" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Размер" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Veľkosť" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Velikost" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Boyut" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Розмір" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Dung lượng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "大小" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "大小" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "大小" } } } }, "Size: %@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Größe: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Tamaño: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Taille : %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ukuran: %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Dimensione: %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "サイズ: %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "크기: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Rozmiar: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Tamanho: %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Tamanho: %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Размер: %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Veľkosť: %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Velikost: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Boyut: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Розмір: %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Kích thước: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "大小:%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "大小:%@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "大小:%@" } } } }, "Skip %@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Überspringen %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Omitir %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Passer %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lewati %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Salta %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "スキップ %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "건너뛰기 %@@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pomiń %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Pular %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Saltar %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Пропустить %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Preskočiť %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Preskoči %@@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Atla %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Пропустити %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Bỏ qua %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "跳过 %@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "跳過 %@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "跳過 %@" } } } }, "Skip This Version" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Diese Version überspringen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Saltar esta versión" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Passer cette version" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lewati Versi Ini" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Salta questa versione" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "このバージョンをスキップ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 버전 건너뛰기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pomiń tę wersję" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Pular esta versão" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ignorar Esta Versão" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Пропустить эту версию" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Preskočiť túto verziu" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Preskoči to različico" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu Sürümü Atla" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Пропустити цю версію" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Bỏ qua phiên bản này" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "跳过此版本" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "跳過此版本" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "跳過此版本" } } } }, "Some of the features that require the helper:" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Einige der Funktionen, die den Helfer erfordern:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Algunas de las funciones que requieren el asistente:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Certaines des fonctionnalités nécessitant l'assistant :" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Beberapa fitur yang memerlukan pembantu:" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Alcune delle funzionalità che richiedono l'assistente:" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ヘルパーが必要な機能の一部:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "도우미가 필요한 기능 중 일부:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Niektóre funkcje wymagające pomocnika:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Algumas das funcionalidades que requerem o assistente:" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Algumas das funcionalidades que requerem o assistente:" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Некоторые функции, требующие помощника:" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Niektoré z funkcií, ktoré vyžadujú pomocníka:" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Nekatere funkcije, ki zahtevajo pomočnika:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yardımcı gerektiren bazı özellikler:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Деякі функції, які потребують помічника:" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Một số tính năng cần trình trợ giúp:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "需要助手的某些功能:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "需要助手的某些功能:" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "需要助手的某些功能:" } } } }, "Sort app list alphabetically by name or by size" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Programm-Liste alphabetisch nach Namen oder nach Grösse sortieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ordenar la lista de aplicaciones alfabéticamente por nombre o por tamaño" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Trier la liste des applications par ordre alphabétique, par nom ou par taille" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Urutkan daftar aplikasi secara alfabetis berdasarkan nama atau ukuran" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ordina l'elenco delle app alfabeticamente per nome o per dimensione" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アプリリストを名前またはサイズでアルファベット順に並べ替え" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "앱 목록을 이름 또는 크기별로 알파벳 순으로 정렬" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Sortuj listę aplikacji alfabetycznie według nazwy lub rozmiaru" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Classificar a lista de aplicativos alfabeticamente por nome ou por tamanho" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ordenar a lista de aplicações alfabeticamente por nome ou por tamanho" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Отсортировать список приложений в алфавитном порядке по названию или размеру" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zoradiť zoznam aplikácií abecedne podľa názvu alebo veľkosti" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Razvrsti seznam aplikacij po abecedi po imenu ali po velikosti" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uygulama listesini ada veya boyuta göre alfabetik olarak sırala" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Сортувати список програм за алфавітом, назвою або розміром" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Sắp xếp danh sách ứng dụng theo thứ tự chữ cái (a-z) hoặc theo kích thước" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "按名称或大小按字母顺序排序应用列表" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "依字母順序,按名稱或大小排序應用程式列表" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "依字母順序,按名稱或大小排序應用程式列表" } } } }, "Sort by %@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Sortieren nach %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ordenar por %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Trier par %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Urutkan berdasarkan %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ordina per %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%@で並べ替え" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@별 정렬" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Sortuj według %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ordenar por %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ordenar por %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Сортировать по %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zoradiť podľa %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Razvrsti po %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%@ göre sırala" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Сортувати за %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Sắp xếp theo %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "按%@排序" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "排序依據 %@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "按%@排序" } } } }, "Sort by %@. Click to cycle through options" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Sortieren nach %@. Klicken, um durch die Optionen zu wechseln" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ordenar por %@. Haga clic para alternar entre las opciones" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Trier par %@. Cliquez pour faire défiler les options" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Urutkan berdasarkan %@. Klik untuk menggilir opsi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ordina per %@. Clicca per scorrere le opzioni" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%@ で並べ替え。オプションを切り替えるにはクリック" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@ 기준으로 정렬. 옵션을 순환하려면 클릭하세요" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Sortuj według %@. Kliknij, aby przeglądać opcje" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ordenar por %@. Clique para alternar entre as opções" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ordenar por %@. Clique para alternar entre as opções" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Сортировать по %@. Нажмите, чтобы переключать варианты" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zoradiť podľa %@. Kliknite pre prechádzanie možnosťami" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Razvrsti po %@. Kliknite za preklop med možnostmi" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%@ ile sırala. Seçenekler arasında geçiş yapmak için tıklayın" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Сортувати за %@. Натисніть, щоб переглянути варіанти" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Sắp xếp theo %@. Nhấp để chuyển qua các tùy chọn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "按 %@ 排序。点击以循环浏览选项" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "按 %@ 排序。點擊以循環選項" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "按 %@ 排序。點擊以循環選項" } } } }, "Sorted by Name" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Sortiert nach Namen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ordenado por Nombre" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Trié par nom" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Diurutkan berdasarkan Nama" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ordinato per Nome" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "名前で並べ替え" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이름순으로 정렬" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Posortowane według nazwy" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ordenado por Nome" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ordenado por Nome" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Отсортировано по названию" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zoradené podľa názvu" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Razvrščeno po imenu" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İsme Göre Sıralandı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Відсортовано за Назвою" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đã sắp xếp theo tên" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "按名称排序" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "按照名稱排序" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "按照名稱排序" } } } }, "Sorted by Size" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Sortiert nach Größe" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ordenado por tamaño" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Trié par taille" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Diurutkan berdasarkan Ukuran" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ordinato per dimensione" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "サイズで並べ替え" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "크기별 정렬" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Posortowane według rozmiaru" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ordenado por Tamanho" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ordenado por Tamanho" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Отсортировано по размеру" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zoradené podľa veľkosti" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Razvrščeno po velikosti" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Boyuta Göre Sıralandı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Відсортовано за Розміром" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đã sắp xếp theo kích thước" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "按大小排序" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "按照大小排序" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "按照大小排序" } } } }, "Sorting" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Sortierung" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Clasificación" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Tri" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pengurutan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ordinamento" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ソート" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "정렬" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Sortowanie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Classificação" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ordenação" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Сортировка" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Triedenie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Razvrščanje" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sıralama" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Сортування" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Sắp xếp" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "排序" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "排序" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "排序" } } } }, "Sorting Options" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Sortieroptionen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Opciones de ordenación" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Options de tri" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Opsi Pengurutan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Opzioni di ordinamento" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "並べ替えオプション" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "정렬 옵션" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Opcje sortowania" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Opções de Classificação" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Opções de Ordenação" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Опции сортировки" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Možnosti zoradenia" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Možnosti razvrščanja" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sıralama Seçenekleri" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Параметри Сортування" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tủy chọn sắp xếp" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "排序选项" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "排序選項" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "排序選項" } } } }, "Sorting: %@" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Sortierung: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ordenando: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Tri : %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pengurutan: %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ordinamento: %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "並べ替え:%@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "정렬: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Sortowanie: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Classificação: %@\n" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ordenação: %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Сортировка: %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zoradenie: %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Razvrščanje: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sıralama: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Сортування: %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Sắp xếp theo %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "按 %@ 排序" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "排序:%@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "排序:%@" } } } }, "Space Savings: %d%%" : { "comment" : "Lipo result title", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Platzersparnis: %d%%" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ahorro de espacio: %d%%" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Gain de place: %d%%" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Penghematan Ruang: %d%%" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Risparmio di Spazio: %d%%" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "スペース節約: %d%%" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "공간 절약: %d%%" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zaoszczędzone miejsce: %d%%" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Economia de Espaço: %d%%" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Poupança de Espaço: %d%%" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Экономия места: %d%%" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Úspora miesta: %d%%" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prihranek prostora: %d%%" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Alandan Tasarruf: %d%%" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Заощадження місця: %d%%" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tiết kiệm không gian: %d%%" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "空间节省: %d%%" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "節省空間:%d%%" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "節省空間:%d%%" } } } }, "Space Savings: %d%%\nTotal Space Saved: %@" : { "comment" : "Lipo completion title", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Platzersparnis: %d%%\nGesamteingesparter Speicherplatz: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ahorro de espacio: %d%% Espacio total ahorrado: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Économie d'espace : %d%%\nEspace total économisé : %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Penghematan Ruang: %d%% Total Ruang Tersimpan: %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Risparmio di spazio: %d%%\nSpazio totale risparmiato: %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "スペース節約: %d%%\n合計節約スペース: %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "공간 절약: %d%%\n총 절약된 공간: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zaoszczędzone miejsce: %d%%\nCałkowite zaoszczędzone miejsce: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Economia de Espaço: %d%% Espaço Total Economizado: %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Poupança de Espaço: %d%%\nEspaço Total Poupado: %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Экономия места: %d%% Общий объем сэкономленного места: %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Úspora miesta: %d%%\nCelková ušetrená kapacita: %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prihranek prostora: %d%%\nSkupaj prihranjen prostor: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Alan Tasarrufu: %d%%\nToplam Tasarruf Edilen Alan: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Економія простору: %d%% Загальний збережений простір: %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tiết kiệm dung lượng: %d%% \nTổng dung lượng đã tiết kiệm: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "空间节省:%d%% 总节省空间:%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "節省空間:%d%%\n總共節省空間:%@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "節省空間:%d%%\n總共節省空間:%@" } } } }, "Sparkle" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Sparkle" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Sparkle" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Étincelle" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Berkilau" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Scintillio" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "スパークル" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Sparkle" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Sparkle" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Sparkle" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Sparkle" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Sparkle" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Trblietanie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Sparkle" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sparkle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Іскра" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Sparkle" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Sparkle" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Sparkle" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "Sparkle" } } } }, "Sponsor" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Sponsoren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Patrocinador" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Sponsor" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Sponsor" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Sponsor" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "スポンサー" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "후원자" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Sponsor" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Patrocinar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Patrocinador" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Спонсор" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Podporovateľ" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Sponzor" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sponsor" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Спонсор" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ủng hộ nhà phát triển" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "赞助" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "贊助" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "贊助" } } } }, "Spotlight Index" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Spotlight-Index" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Índice de Spotlight" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Index Spotlight" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Indeks Spotlight" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Indice Spotlight" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "スポットライトインデックス" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Spotlight 인덱스" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Indeks Spotlight" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Índice do Spotlight" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Índice do Spotlight" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Индекс Spotlight" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Index vyhľadávania Spotlight" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Indeks Spotlight" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Spotlight Dizini" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Індекс Spotlight" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chỉ mục Spotlight" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "Spotlight 索引" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "Spotlight 索引" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "Spotlight 索引" } } } }, "Start" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Start" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Iniciar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Démarrer" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Mulai" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Avvia" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "開始" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "시작" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Rozpocznij" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Iniciar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Iniciar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Запуск" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Štart" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Začni" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Başlat" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Почати" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Bắt đầu" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "开始" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "開始" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "開始" } } } }, "Start Lipo" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Lipo starten" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Comenzar Lipo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Démarrer Lipo" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Mulai Lipo" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Avvia Lipo" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Lipoを始める" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "번들 축소 시작" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Rozpocznij Lipo" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Iniciar Lipo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Iniciar Lipo" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Запустить Lipo" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Spustiť Lipo" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Začni Lipo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Lipo'yu Başlat" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Почати lipo" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Khởi động Lipo" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "开始瘦身" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "開始節省" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "開始節省" } } } }, "Startup Disk" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Startvolume" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Disco de arranque" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Disque de démarrage" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Disk Startup" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Disco di avvio" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "起動ディスク" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "시동 디스크" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dysk startowy" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Disco de Inicialização" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Disco de Inicialização" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Загрузочный диск" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Štartovací disk" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Zagonski disk" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Başlangıç Diski" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Завантажувальний диск" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đĩa Khởi động" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "启动磁盘" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "啟動磁碟" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "啟動磁碟" } } } }, "Startup view & page visibility" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Startansicht & Seiten-Sichtbarkeit" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Vista de inicio y visibilidad de la página" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Vue de démarrage et visibilité de la page" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tampilan awal & visibilitas halaman" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Vista di avvio e visibilità della pagina" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "スタートアップビューとページの可視性" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "시작시 " } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Widok startowy i widoczność strony" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Visualização inicial e visibilidade da página" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Vista inicial e visibilidade da página" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Вид при запуске и видимость страницы" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobrazenie pri spustení a viditeľnosť stránky" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Začetni pogled in vidnost strani" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Başlangıç görünümü ve sayfa görünürlüğü" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Перегляд при запуску та видимість сторінки" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chế độ xem khởi động & khả năng hiển thị trang" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "启动视图和页面可见性" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "啟動視圖和頁面可見性" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "啟動視圖及頁面可見性" } } } }, "Statistics" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Statistiken" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Estadísticas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Statistiques" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Statistik" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Statistiche" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "統計" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "통계" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Statystyki" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Estatísticas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Estatísticas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Статистика" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Štatistiky" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Statistika" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İstatistikler" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Статистика" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thống kê" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "统计" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "統計" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "統計" } } } }, "Status" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Status" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Estado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Statut" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Status" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Stato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ステータス" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "상태" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Status" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Status" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Estado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Статус" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Status" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Status" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Durum" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Статус" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Trạng thái" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "状态" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "狀態" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "狀態" } } } }, "Status:" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Status:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Estado" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Statut:" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Status:" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Stato:" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ステータス:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "상태:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Status:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Status:" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Estado:" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Статус:" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Stav:" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Status:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Durum:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Статус:" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Trạng thái:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "状态:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "狀態:" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "狀態:" } } } }, "Stop" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Stopp" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Detener" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Arrêter" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Berhenti" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Ferma" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "停止" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "중지" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zatrzymaj" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Parar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Parar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Остановить" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zastaviť" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ustavi" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Durdur" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Зупинити" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Dừng lại" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "停止" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "停止" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "停止" } } } }, "Stop checking for updates" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Überprüfung auf Updates stoppen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Dejar de buscar actualizaciones" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Arrêter de vérifier les mises à jour" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Berhenti memeriksa pembaruan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Smettere di controllare gli aggiornamenti" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "更新の確認を停止" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "업데이트 확인 중지" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zatrzymaj sprawdzanie aktualizacji" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Parar de verificar atualizações" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Parar de verificar atualizações" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Прекратить проверку обновлений" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zastaviť kontrolu aktualizácií" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prenehaj preverjati za posodobitve" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Güncellemeleri kontrol etmeyi durdur" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Припинити перевірку оновлень" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Dừng kiểm tra cập nhật" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "停止检查更新" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "停止檢查更新" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "停止檢查更新" } } } }, "Strict" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Streng" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Estricto" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Strict(e)" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ketat" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Rigido" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "厳密" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "엄격" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ścisły" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Estrito" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Rigoroso" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Строгий" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Prísne" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Strogo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sıkı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Суворий" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Nghiêm ngặt" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "严格" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "嚴格" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "嚴格" } } } }, "Submit a bug or feature request" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Einen Fehler oder eine Funktionsanfrage einreichen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Enviar un informe de error o solicitud de función" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Soumettre un bug ou une demande de fonctionnalité" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Kirim bug atau permintaan fitur" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Invia un bug o una richiesta di funzionalità" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "バグ報告または機能リクエストを送信" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "버그 또는 기능 요청 제출하기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zgłoś błąd lub prośbę o dodanie funkcji" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Enviar um bug ou solicitação de recurso" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Enviar um bug ou pedido de funcionalidade" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Сообщить об ошибке или предложить функцию" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nahlásiť chybu alebo požiadavku na funkciu" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pošlji napako ali zahtevo za funkcijo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bir özellik isteği veya hata bildir" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Подати запит на виправлення помилки або новий функціонал" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Phản hồi lỗi xảy ra khi sử dụng ứng dụng hoặc yêu cầu thêm tính năng mới" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "提交错误或功能请求" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "送出一个錯誤回報或功能要求" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "送出一个錯誤回報或功能要求" } } } }, "Submit New Issue" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Neuen Fehler melden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Enviar nuevo problema" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Soumettre un nouveau problème" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Kirim Laporan Masalah Baru" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Invia Nuovo Problema" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "新しい問題を送信" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "새 이슈 제출" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zgłoś nowy problem" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Enviar Novo Problema" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Submeter Novo Problema" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Сообщить о новой проблеме" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nahlásiť nový problém" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pošlji novo težavo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yeni Sorun Bildir" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Повідомити про проблему" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Gửi báo cáo lỗi mới" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "提交新问题" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "送出新 Issue" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "送出新 Issue" } } } }, "Support" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Unterstützung" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Soporte" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Assistance" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Dukungan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Supporto" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "サポート" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "지원" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wsparcie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Suporte" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Suporte" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Поддержка" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Podpora" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Podpora" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Destek" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Підтримка" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hỗ trợ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "支持" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "支援" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "支援" } } } }, "Switch to categorized view" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Zur kategorisierten Ansicht wechseln" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cambiar a vista categorizada" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Passer à la vue catégorisée" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Beralih ke tampilan terkategori" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Passa alla vista categorizzata" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "カテゴリ別ビューに切り替え" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "분류된 보기로 전환" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Przełącz na widok kategoryzowany" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Mudar para a visualização categorizada" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Mudar para a vista categorizada" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Переключиться на категоризированный вид" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Prepnúť na kategorizovaný pohľad" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Preklopi na kategoriziran pogled" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kategorize edilmiş görünüme geç" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Переключитися на категоризований вигляд" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chuyển sang chế độ xem phân loại" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "切换到分类视图" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "切換到分類檢視" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "切換到分類檢視" } } } }, "Switch to simple view" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Zur einfachen Ansicht wechseln" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cambiar a vista simple" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Passer à la vue simple" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Beralih ke tampilan sederhana" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Passa alla vista semplice" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "シンプルビューに切り替え" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "간단한 보기로 전환" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Przełącz na prosty widok" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Mudar para visualização simples" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Mudar para a vista simples" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Переключиться на упрощенный вид" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Prepnúť na jednoduché zobrazenie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Preklopi na preprost pogled" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Basit görünüme geç" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Переключитися на простий вигляд" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chuyển sang chế độ xem đơn giản" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "切换到简单视图" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "切換到簡單視圖" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "切換至簡單視圖" } } } }, "Switch Utilities" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dienstprogramme wechseln" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cambiar Utilidades" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Utilitaires de commutation" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Utilitas Sakelar" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Cambia utilità" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ユーティリティを切り替える" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "유틸리티 전환" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Przełącz narzędzia" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Alternar Utilitários" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Alternar Utilitários" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Переключить утилиты" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Prepínať nástroje" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Preklopi pripomočke" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yardımcı Programları Değiştir" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Перемикач утиліт" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chuyển Tiện ích" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "切换工具" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "切換工具" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "切換工具" } } } }, "system" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "System" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "sistema" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "système" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "sistem" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "sistema" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "システム" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "시스템" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "system" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "sistema" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "sistema" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "система" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "systém" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "sistem" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "sistem" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "система" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "hệ thống" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "系统" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "系統" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "系統" } } } }, "System" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "System" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Sistema" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Système" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Sistem" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Sistema" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "システム" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "시스템" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "System" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Sistema" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Sistema" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Система" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Systém" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Sistem" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sistem" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Система" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hệ thống" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "系统" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "系統" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "系統" } } } }, "Tag name" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Tag-Name" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Nombre de etiqueta" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Nom de la balise" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Nama tag" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nome etichetta" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "タグ名" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "태그 이름" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nazwa tagu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nome da tag" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Nome da etiqueta" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Имя тега" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Názov značky" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ime oznake" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Etiket adı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ім'я тега" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tên thẻ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "标签名称" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "標籤名稱" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "標籤名稱" } } } }, "Tag names (comma-separated)" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Tag-Namen (durch Kommas getrennt)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Nombres de etiquetas (separados por comas)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Noms de balises (séparés par des virgules)" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Nama tag (dipisahkan dengan koma)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Nomi dei tag (separati da virgola)" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "タグ名(カンマ区切り)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "태그 이름 (쉼표로 구분)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nazwy tagów (oddzielone przecinkami)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Nomes de tags (separados por vírgula)" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Nomes de etiquetas (separados por vírgulas)" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Названия тегов (через запятую)" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Názvy značiek (oddelené čiarkou)" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Imena oznak (ločena z vejicami)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Etiket adları (virgülle ayrılmış)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Назви тегів (через кому)" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tên thẻ (ngăn cách bằng dấu phẩy)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "标签名称(以逗号分隔)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "標籤名稱(以逗號分隔)" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "標籤名稱(以逗號分隔)" } } } }, "Tags" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Tags" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Etiquetas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Tags" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tag" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Tag" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "タグ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "태그" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Tagi" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Tags" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Etiquetas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Теги" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Značky" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Oznake" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Etiketler" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Теги" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thẻ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "标签" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "標籤" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "標籤" } } } }, "Taps" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Wasserhähne" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Grifos" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Robinets" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Keran" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Rubinetti" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "蛇口" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "탭" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Kranów" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Torneiras" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Toques" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Краны" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Kohútiky" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pipes" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Musluklar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Крани" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Vòi nước" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "水龙头" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "水龍頭" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "水龍頭" } } } }, "The search sensitivity level controls how strict or lenient Pearcleaner is when finding related files for an app:\n\n• Strict – %@\n\n• Enhanced – %@\n\n• Deep – %@\n\nAt levels higher than Strict it is recommended to check found files manually." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Die Suchsensitivitätsstufe steuert, wie streng oder nachsichtig Pearcleaner bei der Suche nach zugehörigen Dateien für eine App ist:\n\n• Streng – %1$@\n\n• Erweitert – %2$@\n\n• Tief – %3$@\n\nBei Stufen über Streng wird empfohlen, gefundene Dateien manuell zu überprüfen." } }, "en" : { "stringUnit" : { "state" : "new", "value" : "The search sensitivity level controls how strict or lenient Pearcleaner is when finding related files for an app:\n\n• Strict – %1$@\n\n• Enhanced – %2$@\n\n• Deep – %3$@\n\nAt levels higher than Strict it is recommended to check found files manually." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "El nivel de sensibilidad de búsqueda controla cuán estricto o flexible es Pearcleaner al encontrar archivos relacionados para una aplicación:\n\n• Estricto – %@\n\n• Mejorado – %@\n\n• Profundo – %@\n\nEn niveles superiores a Estricto se recomienda verificar manualmente los archivos encontrados." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Le niveau de sensibilité de la recherche contrôle la rigueur ou la tolérance de Pearcleaner lors de la recherche de fichiers associés pour une application :\n\n• Strict – %1$@\n\n• Amélioré – %2$@\n\n• Profond – %3$@\n\nÀ des niveaux supérieurs à Strict, il est recommandé de vérifier manuellement les fichiers trouvés." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tingkat sensitivitas pencarian mengontrol seberapa ketat atau longgar Pearcleaner saat menemukan file terkait untuk sebuah aplikasi:\n\n• Ketat – %1$@\n\n• Ditingkatkan – %2$@\n\n• Mendalam – %3$@\n\nPada tingkat yang lebih tinggi dari Ketat, disarankan untuk memeriksa file yang ditemukan secara manual." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Il livello di sensibilità della ricerca controlla quanto rigoroso o indulgente sia Pearcleaner nel trovare file correlati per un'app:\n\n• Rigoroso – %@\n\n• Avanzato – %@\n\n• Profondo – %@\n\nA livelli superiori a Rigoroso è consigliato controllare manualmente i file trovati." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "検索感度レベルは、アプリの関連ファイルを見つける際の Pearcleaner の厳格さまたは寛容さを制御します:\n\n• 厳格 – %@\n\n• 強化 – %@\n\n• 深度 – %@\n\n厳格以上のレベルでは、見つかったファイルを手動で確認することをお勧めします。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "검색 민감도 수준은 Pearcleaner가 앱과 관련된 파일을 찾을 때 얼마나 엄격하거나 관대한지를 제어합니다:\n\n• 엄격 – %@\n\n• 향상 – %@\n\n• 심층 – %@\n\n엄격 이상의 수준에서는 발견된 파일을 수동으로 확인하는 것이 좋습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Poziom czułości wyszukiwania kontroluje, jak rygorystyczny lub elastyczny jest Pearcleaner przy znajdowaniu powiązanych plików dla aplikacji:\n\n• Rygorystyczny – %@\n\n• Ulepszony – %@\n\n• Głęboki – %@\n\nNa poziomach wyższych niż Rygorystyczny zaleca się ręczne sprawdzenie znalezionych plików." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "O nível de sensibilidade da pesquisa controla quão rigoroso ou flexível o Pearcleaner é ao encontrar arquivos relacionados para um aplicativo:\n\n• Rigoroso – %@\n\n• Aprimorado – %@\n\n• Profundo – %@\n\nEm níveis superiores a Rigoroso, é recomendável verificar manualmente os arquivos encontrados." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "O nível de sensibilidade da pesquisa controla quão rigoroso ou flexível o Pearcleaner é ao encontrar arquivos relacionados para um aplicativo:\n\n• Rigoroso – %1$@\n\n• Aumentado – %2$@\n\n• Profundo – %3$@\n\nEm níveis superiores a Rigoroso, é recomendado verificar os arquivos encontrados manualmente." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Уровень чувствительности поиска контролирует, насколько строго или мягко Pearcleaner находит связанные файлы для приложения:\n\n• Строгий – %@\n\n• Улучшенный – %@\n\n• Глубокий – %@\n\nНа уровнях выше Строгого рекомендуется проверять найденные файлы вручную." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Úroveň citlivosti vyhľadávania určuje, aký prísny alebo zhovievavý je Pearcleaner pri hľadaní súvisiacich súborov pre aplikáciu:\n\n• Prísny – %1$@\n\n• Vylepšený – %2$@\n\n• Hlboký – %3$@\n\nNa úrovniach vyšších ako Prísny sa odporúča manuálne skontrolovať nájdené súbory." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Raven iskanja določa, kako strogo ali popustljivo je Pearcleaner pri iskanju povezanih datotek za aplikacijo:\n\n• Strogo – %@\n\n• Izboljšano – %@\n\n• Globoko – %@\n\nNa ravneh višjih od Strogo je priporočljivo ročno preveriti najdene datoteke." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Arama hassasiyet seviyesi, Pearcleaner'ın bir uygulama için ilgili dosyaları bulurken ne kadar katı veya esnek olduğunu kontrol eder:\n\n• Katı – %@\n\n• Gelişmiş – %@\n\n• Derin – %@\n\nKatı seviyesinin üzerindeki seviyelerde bulunan dosyaların manuel olarak kontrol edilmesi önerilir." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Рівень чутливості пошуку контролює, наскільки суворо або поблажливо Pearcleaner знаходить пов'язані файли для програми:\n\n• Суворий – %@\n\n• Покращений – %@\n\n• Глибокий – %@\n\nНа рівнях вище Суворого рекомендується перевіряти знайдені файли вручну." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Mức độ nhạy cảm của tìm kiếm kiểm soát mức độ nghiêm ngặt hoặc linh hoạt của Pearcleaner khi tìm kiếm các tệp liên quan cho một ứng dụng:\n\n• Nghiêm ngặt – %@\n\n• Nâng cao – %@\n\n• Sâu – %@\n\nỞ các mức cao hơn Nghiêm ngặt, nên kiểm tra thủ công các tệp được tìm thấy." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "搜索敏感度级别控制 Pearcleaner 在寻找应用程序相关文件时的严格或宽松程度:\n\n• 严格 – %@\n\n• 增强 – %@\n\n• 深入 – %@\n\n在高于严格的级别,建议手动检查找到的文件。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "搜尋敏感度等級控制 Pearcleaner 在尋找應用程式相關檔案時的嚴格或寬鬆程度:\n\n• 嚴格 – %1$@\n\n• 增強 – %2$@\n\n• 深度 – %3$@\n\n在高於嚴格的等級時,建議手動檢查找到的檔案。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "搜尋敏感度級別控制 Pearcleaner 在尋找應用程式相關檔案時的嚴格或寬鬆程度:\n\n• 嚴格 – %@\n\n• 增強 – %@\n\n• 深入 – %@\n\n在高於嚴格的級別,建議手動檢查找到的檔案。" } } } }, "The total space savings between all the lipo'd apps\nSize Before: %@\nSize After: %@" : { "comment" : "Lipo completion message", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Der gesamte Speicherplatz, der durch alle lipo'd Apps eingespart wurde \nGröße vorher: %@ \nGröße nachher: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "El ahorro total de espacio entre todas las aplicaciones lipo'd Tamaño antes: %@ Tamaño después: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "L'économie totale d'espace entre toutes les applications lipo'd Taille avant : %@ Taille après : %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Total penghematan ruang antara semua aplikasi yang telah di-lipo\nUkuran Sebelum: %@\nUkuran Sesudah: %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Il risparmio totale di spazio tra tutte le app ridotte\nDimensione Prima: %@\nDimensione Dopo: %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "すべてのlipoされたアプリ間の合計スペース節約量\nサイズ前: %@\nサイズ後: %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "번들 축소한 앱의 총 공간 절약\n이전 크기: %@\n이후 크기: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Całkowite zaoszczędzone miejsce pomiędzy wszystkimi aplikacjami poddanymi lipo\nRozmiar przed: %@\nRozmiar po: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "A economia total de espaço entre todos os aplicativos lipo'd Tamanho Antes: %@ Tamanho Depois: %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A poupança total de espaço entre todas as apps lipo'd\nTamanho Antes: %@\nTamanho Depois: %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Общая экономия места между всеми lipo'd приложениями Размер до: %@ Размер после: %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Celková úspora miesta medzi všetkými lipo aplikáciami\nVeľkosť pred: %@\nVeľkosť po: %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Skupni prihranek prostora med vsemi lipo aplikacijami\nVelikost prej: %@\nVelikost po: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Tüm lipo'lanmış uygulamalar arasındaki toplam alan tasarrufu\nÖnceki Boyut: %@\nSonraki Boyut: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Загальна економія місця між усіма об'єднаними додатками Розмір до: %@ Розмір після: %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tổng dung lượng tiết kiệm giữa tất cả các ứng dụng đã lipo Kích thước trước: %@ Kích thước sau: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "所有精简应用的总空间节省\n精简前大小:%@\n精简后大小:%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "所有經過 lipo 處理的應用程式之間的總空間節省\n處理前大小:%@\n處理後大小:%@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "所有已進行 lipo 處理的應用程式總共節省的空間\n處理前大小:%@\n處理後大小:%@" } } } }, "Theme" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Design" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Tema" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Thème" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tema" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Tema" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "テーマ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "테마" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Motyw" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Tema" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Tema" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Тема" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Motív" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Tema" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Tema" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Тема" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chủ đề" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "主题" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "主題" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "主題" } } } }, "Theme Mode" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Themen Modus" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Modo de tema" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mode thème" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Mode Tema" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Modalità Tema" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "テーマモード" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "테마 모드" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Tryb motywu" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Modo de Tema" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Modo de Tema" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Режим темы" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Téma vzhľadu" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Način teme" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Tema Modu" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Режим Теми" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chủ đề" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "主题模式" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "主題模式" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "主題模式" } } } }, "There are no files to remove" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Es gibt keine Dateien zum Entfernen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No hay archivos para eliminar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Il n'y a pas de fichiers à supprimer" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak ada file untuk dihapus" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Non ci sono file da rimuovere" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "削除するファイルがありません" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "제거할 파일이 없습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nie ma plików do usunięcia" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Não há arquivos para remover" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Não há ficheiros para remover" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Нет файлов для удаления" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nie sú žiadne súbory na odstránenie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ni datotek za odstranitev" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kaldırılacak dosya yok" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Немає файлів для видалення" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không có tệp nào để xóa" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "没有文件可删除" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "沒有檔案可刪除" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "沒有檔案可刪除" } } } }, "Third-party" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Drittanbieter" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Tercero" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Tiers" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pihak ketiga" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Terze parti" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "サードパーティ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "타사" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Strona trzecia" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Terceiro" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Terceiros" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Сторонний" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Tretia strana" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Tretja oseba" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Üçüncü taraf" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Сторонній" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Bên thứ ba" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "第三方" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "第三方" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "第三方" } } } }, "This adds the file/folder to the Exclusions list. Edit the exclusions list from Settings > Folders tab" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Hiermit werden Dateien bzw. Ordner zur Ausschlussliste hinzugefügt. Bearbeiten Sie die Ausschlussliste unter Einstellungen > Registerkarte Ordner" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Esto añade el archivo/carpeta a la lista de exclusiones Edita la lista de exclusiones desde Configuración > pestaña Carpetas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Cela ajoute le fichier/dossier à la liste des exclusions. Modifiez la liste des exclusions depuis Paramètres > Onglet Dossiers" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ini menambahkan file/folder ke daftar Pengecualian. Edit daftar pengecualian dari Pengaturan > tab Folder" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Questo aggiunge il file/cartella all'elenco delle esclusioni. Modifica l'elenco delle esclusioni da Impostazioni > Scheda Cartelle" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "これにより、ファイル/フォルダーが除外リストに追加されます。除外リストは、設定 > フォルダータブから編集できます" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 파일/폴더를 제외 목록에 추가합니다. 제외 목록은 설정 > 폴더 탭에서 편집할 수 있습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Spowoduje to dodanie pliku/folderu do listy wykluczeń. Listę wykluczeń można edytować w zakładce Ustawienia > Foldery" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Isso adiciona o arquivo/pasta à lista de Exclusões. Edite a lista de exclusões em Configurações > Aba Pastas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Isto adiciona o ficheiro/pasta à lista de Exclusões. Edite a lista de exclusões em Definições > separador Pastas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Это добавит файл/папку в список исключений. Отредактируйте список исключений на вкладке \"Настройки\" > \"Папки\"." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Toto pridá súbor/priečinok do zoznamu výnimiek. Zoznam výnimiek môžeš upraviť v Nastaveniach > PriečinkyToto pridá súbor/priečinok do zoznamu výnimiek. Zoznam výnimiek môžeš upraviť v Nastaveniach > Priečinky" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "To doda datoteko/mapo na seznam izjem. Uredite seznam izjem v Nastavitve > Zavihek Mape" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu, dosyayı/klasörü Hariç Tutulanlar listesine ekler. Hariç Tutulanlar listesini Ayarlar > Klasörler sekmesinden düzenleyin" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Файл/папку буде додано до списку винятків. Відредагуйте список винятків на вкладці Налаштування > вкладка Папки" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thao tác này sẽ thêm tệp/thư mục vào danh sách loại trừ. Bạn có thể chỉnh sửa danh sách loại trừ trong Cài đặt > Thẻ Thư mục." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "这将文件/文件夹添加到排除列表。请从设置 > 文件夹标签中编辑排除列表" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "這樣會將檔案/資料夾加至排除列表中。透過「設定」>「資料夾」分頁編輯排除列表" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "這樣會將檔案/資料夾加至排除列表中。透過「設定」>「資料夾」分頁編輯排除列表" } } } }, "This app has no removable language translation files." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Diese App hat keine entfernbaren Sprachübersetzungsdateien." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Esta aplicación no tiene archivos de traducción de idiomas removibles." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Cette application n'a pas de fichiers de traduction de langue amovibles." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Aplikasi ini tidak memiliki file terjemahan bahasa yang dapat dihapus." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Questa app non ha file di traduzione linguistica rimovibili." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "このアプリには削除可能な言語翻訳ファイルがありません。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 앱에는 제거 가능한 언어 번역 파일이 없습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ta aplikacja nie ma usuwalnych plików tłumaczeń językowych." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Este aplicativo não possui arquivos de tradução de idioma removíveis." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Esta aplicação não tem ficheiros de tradução de idiomas removíveis." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "В этом приложении нет файлов переводов, которые можно удалить." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Táto aplikácia nemá žiadne odstrániteľné súbory prekladu jazykov." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ta aplikacija nima odstranljivih jezikovnih prevodnih datotek." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu uygulamanın kaldırılabilir dil çeviri dosyaları yok." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Цей додаток не має знімних файлів перекладу мов." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ứng dụng này không có tệp dịch ngôn ngữ có thể tháo rời." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "此应用程序没有可移除的语言翻译文件。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "此應用程式沒有可移除的語言翻譯檔案。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "此應用程式沒有可移除的語言翻譯檔案。" } } } }, "This app is located in %@" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Das Programm befindet sich in %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Esta aplicación se encuentra en %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Cette application est située dans %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Aplikasi ini terletak di %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Questa app si trova in %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "このアプリは%@にあります" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 앱은 %@에 위치해 있습니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ta aplikacja znajduje się w %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Este aplicativo está localizado em %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Esta aplicação está localizada em %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Это приложение находится в %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Táto aplikácia sa nachádza v %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ta aplikacija se nahaja v %@." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu uygulama %@ konumunda" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ця програма знаходиться в %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ứng dụng này nằm tại %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "此应用程序位于 %@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "此 App 位於 %@ 中" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "此 App 位於 %@ 中" } } } }, "This app was installed from the App Store. Updating it via %@ will:\n• Break the App Store receipt and licensing\n• Remove the app from App Store tracking\n• Prevent future App Store updates\n\nProceed with caution." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Diese App wurde aus dem App Store installiert. Ein Update über %@ wird:\n• Den App Store-Beleg und die Lizenz ungültig machen\n• Die App aus der App Store-Verfolgung entfernen\n• Zukünftige App Store-Updates verhindern\n\nVorsicht walten lassen." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Esta aplicación fue instalada desde el App Store. Actualizarla a través de %@:\n• Romperá el recibo y la licencia del App Store\n• Eliminará la aplicación del seguimiento del App Store\n• Impedirá futuras actualizaciones del App Store\n\nProceda con precaución." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Cette application a été installée depuis l'App Store. La mettre à jour via %@ entraînera :\n• La rupture du reçu et de la licence de l'App Store\n• La suppression de l'application du suivi de l'App Store\n• L'impossibilité de recevoir de futures mises à jour de l'App Store\n\nProcédez avec prudence." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Aplikasi ini diinstal dari App Store. Memperbarui melalui %@ akan:\n• Merusak tanda terima dan lisensi App Store\n• Menghapus aplikasi dari pelacakan App Store\n• Mencegah pembaruan App Store di masa depan\n\nLanjutkan dengan hati-hati." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Questa app è stata installata dall'App Store. Aggiornarla tramite %@:\n• Invaliderà la ricevuta e la licenza dell'App Store\n• Rimuoverà l'app dal tracciamento dell'App Store\n• Impedirà futuri aggiornamenti dall'App Store\n\nProcedere con cautela." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "このアプリはApp Storeからインストールされました。%@を介して更新すると、以下のことが発生します:\n• App Storeのレシートとライセンスが無効になります\n• App Storeのトラッキングからアプリが削除されます\n• 将来のApp Storeの更新ができなくなります\n\n注意して進めてください。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 앱은 App Store에서 설치되었습니다. %@를 통해 업데이트하면 다음과 같은 일이 발생합니다:\n• App Store 영수증 및 라이선스가 손상됩니다\n• App Store 추적에서 앱이 제거됩니다\n• 향후 App Store 업데이트가 불가능해집니다\n\n주의해서 진행하십시오." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ta aplikacja została zainstalowana z App Store. Aktualizacja przez %@ spowoduje:\n• Uszkodzenie paragonu i licencji App Store\n• Usunięcie aplikacji z śledzenia App Store\n• Uniemożliwienie przyszłych aktualizacji App Store\n\nPostępuj ostrożnie." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Este aplicativo foi instalado da App Store. Atualizá-lo via %@ irá:\n• Quebrar o recibo e a licença da App Store\n• Remover o aplicativo do rastreamento da App Store\n• Impedir futuras atualizações da App Store\n\nProceda com cautela." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Esta aplicação foi instalada a partir da App Store. Atualizá-la através de %@ irá:\n• Quebrar o recibo e a licença da App Store\n• Remover a aplicação do rastreamento da App Store\n• Impedir futuras atualizações da App Store\n\nProceda com cautela." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Это приложение было установлено из App Store. Обновление через %@:\n• Нарушит квитанцию и лицензию App Store\n• Удалит приложение из отслеживания App Store\n• Предотвратит будущие обновления App Store\n\nДействуйте с осторожностью." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Táto aplikácia bola nainštalovaná z App Store. Aktualizácia cez %@:\n• Zruší potvrdenie a licenciu App Store\n• Odstráni aplikáciu zo sledovania App Store\n• Zabráni budúcim aktualizáciám App Store\n\nPokračujte opatrne." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ta aplikacija je bila nameščena iz App Store. Posodobitev prek %@ bo:\n• Prekinila potrdilo in licenco App Store\n• Odstranila aplikacijo iz sledenja App Store\n• Preprečila prihodnje posodobitve App Store\n\nNadaljujte previdno." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu uygulama App Store'dan yüklendi. %@ ile güncellemek:\n• App Store makbuzunu ve lisansını geçersiz kılar\n• Uygulamayı App Store takibinden kaldırır\n• Gelecekteki App Store güncellemelerini engeller\n\nDikkatli ilerleyin." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Цей додаток було встановлено з App Store. Оновлення через %@ призведе до:\n• Порушення квитанції та ліцензії App Store\n• Видалення додатка з відстеження App Store\n• Неможливості отримання майбутніх оновлень App Store\n\nДійте обережно." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ứng dụng này được cài đặt từ App Store. Cập nhật qua %@ sẽ:\n• Làm hỏng biên lai và giấy phép của App Store\n• Xóa ứng dụng khỏi theo dõi của App Store\n• Ngăn chặn các bản cập nhật App Store trong tương lai\n\nHãy tiến hành cẩn thận." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "此应用是从 App Store 安装的。通过 %@ 更新将会:\n• 破坏 App Store 收据和许可\n• 从 App Store 跟踪中移除应用\n• 阻止未来的 App Store 更新\n\n请谨慎操作。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "此應用程式是從 App Store 安裝的。通過 %@ 更新將會:\n• 破壞 App Store 的收據和許可\n• 從 App Store 追蹤中移除應用程式\n• 阻止未來的 App Store 更新\n\n請謹慎進行。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "此應用程式是從 App Store 安裝的。透過 %@ 更新將會:\n• 破壞 App Store 收據和許可\n• 從 App Store 追蹤中移除應用程式\n• 阻止未來的 App Store 更新\n\n請小心操作。" } } } }, "This application does not have a supported installer. You may try to adopt it into Homebrew if it exists in the Cask repo." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Diese Anwendung hat keinen unterstützten Installer. Sie können versuchen, sie in Homebrew zu übernehmen, wenn sie im Cask-Repo existiert." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Esta aplicación no tiene un instalador compatible. Puede intentar adoptarla en Homebrew si existe en el repositorio Cask." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Cette application n'a pas d'installateur pris en charge. Vous pouvez essayer de l'adopter dans Homebrew s'il existe dans le dépôt Cask." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Aplikasi ini tidak memiliki penginstal yang didukung. Anda dapat mencoba mengadopsinya ke dalam Homebrew jika ada di repo Cask." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Questa applicazione non ha un programma di installazione supportato. Puoi provare ad adottarla in Homebrew se esiste nel repository Cask." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "このアプリケーションにはサポートされているインストーラーがありません。Caskリポジトリに存在する場合は、Homebrewに採用することを試みることができます。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 애플리케이션은 지원되는 설치 관리자가 없습니다. Cask 저장소에 존재한다면 Homebrew에 채택해볼 수 있습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ta aplikacja nie ma obsługiwanego instalatora. Możesz spróbować zaadoptować ją do Homebrew, jeśli istnieje w repozytorium Cask." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Este aplicativo não possui um instalador compatível. Você pode tentar adotá-lo no Homebrew se ele existir no repositório Cask." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Esta aplicação não tem um instalador suportado. Pode tentar adotá-la no Homebrew se existir no repositório Cask." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "У этого приложения нет поддерживаемого установщика. Вы можете попробовать добавить его в Homebrew, если он существует в репозитории Cask." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Táto aplikácia nemá podporovaný inštalátor. Môžete sa pokúsiť pridať ju do Homebrew, ak existuje v repozitári Cask." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ta aplikacija nima podprtega namestitvenega programa. Poskusite jo lahko vključiti v Homebrew, če obstaja v repozitoriju Cask." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu uygulamanın desteklenen bir yükleyicisi yok. Cask deposunda mevcutsa Homebrew'a eklemeyi deneyebilirsiniz." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Цей додаток не має підтримуваного інсталятора. Ви можете спробувати додати його в Homebrew, якщо він існує в репозиторії Cask." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ứng dụng này không có trình cài đặt được hỗ trợ. Bạn có thể thử đưa nó vào Homebrew nếu nó tồn tại trong kho Cask." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "此应用程序没有受支持的安装程序。如果它存在于 Cask 仓库中,您可以尝试将其纳入 Homebrew。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "此應用程式沒有受支持的安裝程式。如果它存在於 Cask 資料庫中,您可以嘗試將其納入 Homebrew。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "此應用程式沒有受支持的安裝程式。如果它存在於 Cask 資料庫中,您可以嘗試將其納入 Homebrew。" } } } }, "This is a web app" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Das ist eine Web-App" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Esta es una aplicación web" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ceci est une application web" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ini adalah aplikasi web" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Questa è un'app web" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "これはウェブアプリです" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이것은 웹 앱입니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "To jest aplikacja internetowa" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Este é um aplicativo web" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Esta é uma aplicação web" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Это веб-приложение" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Toto je webová aplikácia" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "To je spletna aplikacija" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu bir web uygulamasıdır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Це веб програма" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đây là một ứng dụng web" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "这是一个网络应用程序" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "此為一個網頁 App" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "此為一個網頁 App" } } } }, "This is a wrapped iOS app" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Das ist ein iOS-App" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Esta es una aplicación de iOS envuelta" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ceci est une application iOS empaquetée" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ini adalah aplikasi iOS yang dibungkus" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Questa è un'app iOS avvolta" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "これはラップされたiOSアプリです" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이것은 래핑된 iOS 앱입니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Jest to aplikacja w formie iOS" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Este é um aplicativo iOS empacotado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Este é um aplicativo iOS empacotado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Это приложение создано для iOS" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Toto je zabalená iOS aplikácia" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "To je zavita iOS aplikacija" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu örtülü bir iOS uygulamasıdır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Це обернути iOS програма" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đây là ứng dụng iOS được đóng gói để chạy trên máy Mac" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "这是一个封装的 iOS 应用程序" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "此為一個已封裝的 iOS App" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "此為一個已封裝的 iOS App" } } } }, "This package is signed and verified. The package was cryptographically signed by the developer and its integrity has been verified." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dieses Paket ist signiert und verifiziert. Das Paket wurde kryptografisch vom Entwickler signiert und seine Integrität wurde überprüft." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Este paquete está firmado y verificado. El paquete fue firmado criptográficamente por el desarrollador y su integridad ha sido verificada." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ce paquet est signé et vérifié. Le paquet a été signé cryptographiquement par le développeur et son intégrité a été vérifiée." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Paket ini ditandatangani dan diverifikasi. Paket ini ditandatangani secara kriptografi oleh pengembang dan integritasnya telah diverifikasi." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Questo pacchetto è firmato e verificato. Il pacchetto è stato firmato crittograficamente dallo sviluppatore e la sua integrità è stata verificata." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "このパッケージは署名され、検証されています。パッケージは開発者によって暗号的に署名され、その整合性が検証されています。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 패키지는 서명 및 검증되었습니다. 패키지는 개발자가 암호화 서명하였으며 무결성이 검증되었습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ten pakiet jest podpisany i zweryfikowany. Pakiet został kryptograficznie podpisany przez dewelopera, a jego integralność została zweryfikowana." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Este pacote está assinado e verificado. O pacote foi assinado criptograficamente pelo desenvolvedor e sua integridade foi verificada." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Este pacote está assinado e verificado. O pacote foi criptograficamente assinado pelo desenvolvedor e sua integridade foi verificada." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Этот пакет подписан и проверен. Пакет был криптографически подписан разработчиком, и его целостность была проверена." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Tento balík je podpísaný a overený. Balík bol kryptograficky podpísaný vývojárom a jeho integrita bola overená." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ta paket je podpisan in preverjen. Paket je kriptografsko podpisal razvijalec in njegova celovitost je bila preverjena." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu paket imzalanmış ve doğrulanmıştır. Paket, geliştirici tarafından kriptografik olarak imzalanmış ve bütünlüğü doğrulanmıştır." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Цей пакет підписано та перевірено. Пакет був криптографічно підписаний розробником, і його цілісність була перевірена." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Gói này đã được ký và xác minh. Gói đã được nhà phát triển ký điện tử và tính toàn vẹn của nó đã được xác minh." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "此软件包已签名并验证。该软件包由开发者进行加密签名,其完整性已被验证。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "此套件已簽名並驗證。該套件由開發者進行加密簽名,其完整性已被驗證。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "此套件已簽名並驗證。該套件由開發者進行加密簽名,其完整性已被驗證。" } } } }, "This package is unsigned or unverified. The package was not cryptographically signed, or its signature could not be verified." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dieses Paket ist nicht signiert oder nicht verifiziert. Das Paket wurde nicht kryptografisch signiert oder seine Signatur konnte nicht verifiziert werden." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Este paquete no está firmado o no está verificado. El paquete no fue firmado criptográficamente, o su firma no pudo ser verificada." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Ce paquet n'est pas signé ou non vérifié. Le paquet n'a pas été signé cryptographiquement, ou sa signature n'a pas pu être vérifiée." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Paket ini tidak ditandatangani atau tidak terverifikasi. Paket ini tidak ditandatangani secara kriptografi, atau tanda tangannya tidak dapat diverifikasi." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Questo pacchetto non è firmato o non verificato. Il pacchetto non è stato firmato crittograficamente o la sua firma non può essere verificata." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "このパッケージは署名されていないか、未確認です。パッケージは暗号的に署名されていないか、署名を確認できませんでした。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 패키지는 서명되지 않았거나 검증되지 않았습니다. 패키지가 암호화 서명되지 않았거나 서명을 확인할 수 없습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ten pakiet nie jest podpisany lub niezweryfikowany. Pakiet nie został podpisany kryptograficznie lub jego podpis nie mógł zostać zweryfikowany." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Este pacote não está assinado ou não foi verificado. O pacote não foi assinado criptograficamente, ou sua assinatura não pôde ser verificada." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Este pacote não está assinado ou verificado. O pacote não foi assinado criptograficamente, ou a sua assinatura não pôde ser verificada." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Этот пакет не подписан или не подтвержден. Пакет не был криптографически подписан, или его подпись не удалось проверить." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Tento balík nie je podpísaný alebo overený. Balík nebol kryptograficky podpísaný alebo jeho podpis nebolo možné overiť." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ta paket ni podpisan ali preverjen. Paket ni bil kriptografsko podpisan ali pa njegovega podpisa ni bilo mogoče preveriti." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu paket imzalanmamış veya doğrulanmamış. Paket kriptografik olarak imzalanmamış veya imzası doğrulanamamıştır." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Цей пакет не підписаний або не підтверджений. Пакет не був криптографічно підписаний, або його підпис не вдалося перевірити." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Gói này chưa được ký hoặc chưa được xác minh. Gói không được ký mã hóa, hoặc chữ ký của nó không thể được xác minh." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "此软件包未签名或未验证。该软件包未进行加密签名,或其签名无法验证。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "此套件未簽名或未驗證。該套件未進行加密簽名,或其簽名無法驗證。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "此套件未簽名或未驗證。該套件未進行加密簽名,或其簽名無法驗證。" } } } }, "This will completely uninstall %@ and remove all associated files. This action cannot be undone." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dies wird %@ vollständig deinstallieren und alle zugehörigen Dateien entfernen. Diese Aktion kann nicht rückgängig gemacht werden." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Esto desinstalará completamente %@ y eliminará todos los archivos asociados. Esta acción no se puede deshacer." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Cela désinstallera complètement %@ et supprimera tous les fichiers associés. Cette action ne peut pas être annulée." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ini akan sepenuhnya mencopot pemasangan %@ dan menghapus semua file terkait. Tindakan ini tidak dapat dibatalkan." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Questo disinstallerà completamente %@ e rimuoverà tutti i file associati. Questa azione non può essere annullata." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "これにより、%@ が完全にアンインストールされ、関連するすべてのファイルが削除されます。この操作は元に戻せません。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 작업은 %@를 완전히 제거하고 관련된 모든 파일을 삭제합니다. 이 작업은 되돌릴 수 없습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "To całkowicie odinstaluje %@ i usunie wszystkie powiązane pliki. Tej operacji nie można cofnąć." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Isso desinstalará completamente %@ e removerá todos os arquivos associados. Esta ação não pode ser desfeita." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Isto desinstalará completamente %@ e removerá todos os ficheiros associados. Esta ação não pode ser desfeita." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Это полностью удалит %@ и все связанные файлы. Это действие нельзя отменить." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Týmto sa úplne odinštaluje %@ a odstránia sa všetky súvisiace súbory. Túto akciu nie je možné vrátiť späť." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "To bo popolnoma odstranilo %@ in vse povezane datoteke. Tega dejanja ni mogoče razveljaviti." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu, %@'yı tamamen kaldıracak ve tüm ilgili dosyaları silecektir. Bu işlem geri alınamaz." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Це повністю видалить %@ та всі пов'язані файли. Цю дію не можна скасувати." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Điều này sẽ gỡ cài đặt hoàn toàn %@ và xóa tất cả các tệp liên quan. Hành động này không thể hoàn tác." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "这将完全卸载 %@ 并删除所有相关文件。此操作无法撤销。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "這將完全卸載 %@ 並刪除所有相關文件。此操作無法撤銷。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "這將完全卸載 %@ 並刪除所有相關文件。此操作無法撤銷。" } } } }, "This will exclude selected items from future scans. Exclusion list can be edited from Settings > Folders tab or the sidebar in this view." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dadurch werden ausgewählte Elemente von zukünftigen Scans ausgeschlossen. Die Ausschlussliste kann unter Einstellungen > Ordner-Tab oder in der Seitenleiste in dieser Ansicht bearbeitet werden." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Esto excluirá los elementos seleccionados de futuros análisis. La lista de exclusión se puede editar desde Configuración > pestaña Carpetas o la barra lateral en esta vista." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Cela exclura les éléments sélectionnés des analyses futures. La liste d'exclusion peut être modifiée dans Paramètres > onglet Dossiers ou la barre latérale de cette vue." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ini akan mengecualikan item yang dipilih dari pemindaian di masa depan. Daftar pengecualian dapat diedit dari Pengaturan > tab Folder atau bilah samping di tampilan ini." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Questo escluderà gli elementi selezionati dalle scansioni future. L'elenco di esclusione può essere modificato da Impostazioni > scheda Cartelle o dalla barra laterale in questa vista." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "これにより、選択した項目は今後のスキャンから除外されます。除外リストは、設定 > フォルダー タブまたはこのビューのサイドバーから編集できます。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "선택한 항목을 향후 스캔에서 제외합니다. 제외 목록은 설정 > 폴더 탭 또는 이 보기의 사이드바에서 편집할 수 있습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Spowoduje to wykluczenie wybranych elementów z przyszłych skanowań. Listę wykluczeń można edytować w zakładce Ustawienia > Foldery lub na pasku bocznym w tym widoku." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Isso excluirá os itens selecionados de futuras verificações. A lista de exclusão pode ser editada em Configurações > aba Pastas ou na barra lateral nesta visualização." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Isto excluirá os itens selecionados de futuras verificações. A lista de exclusão pode ser editada em Configurações > Aba de Pastas ou na barra lateral nesta visualização." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Это исключит выбранные элементы из будущих сканирований. Список исключений можно редактировать в Настройки > Вкладка «Папки» или в боковой панели этого окна." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Týmto sa vylúčia vybrané položky z budúcich skenov. Zoznam vylúčení je možné upraviť v Nastaveniach > karta Zložky alebo v bočnom paneli v tomto zobrazení." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "To bo izključilo izbrane elemente iz prihodnjih skeniranj. Seznam izključitev je mogoče urediti v Nastavitve > zavihek Mape ali v stranski vrstici tega pogleda." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu, seçilen öğeleri gelecekteki taramalardan hariç tutacaktır. Hariç tutma listesi Ayarlar > Klasörler sekmesinden veya bu görünümdeki kenar çubuğundan düzenlenebilir." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Це виключить вибрані елементи з майбутніх сканувань. Список виключень можна редагувати в Налаштуваннях > вкладка Папки або на бічній панелі в цьому вигляді." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Điều này sẽ loại trừ các mục đã chọn khỏi các lần quét trong tương lai. Danh sách loại trừ có thể được chỉnh sửa từ Cài đặt > tab Thư mục hoặc thanh bên trong chế độ xem này." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "这将从未来的扫描中排除选定的项目。可以从设置 > 文件夹选项卡或此视图的侧边栏中编辑排除列表" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "這將從未來的掃描中排除選定的項目。可以從設置 > 資料夾標籤或此視圖中的側邊欄編輯排除列表" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "這將從未來的掃描中排除所選項目。排除列表可以從設定 > 資料夾標籤或此視圖的側邊欄編輯。" } } } }, "This will exclude selected items from future scans. Exclusion list can be edited from Settings > Folders tab." : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dadurch werden ausgewählte Elemente von zukünftigen Scans ausgeschlossen. Die Ausschlussliste kann unter „Einstellungen“ > „Ordner“ bearbeitet werden." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Esto excluirá los elementos seleccionados de futuros escaneos La lista de exclusión se puede editar desde Configuración > pestaña Carpetas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Cela exclura les éléments sélectionnés des analyses futures. La liste d'exclusion peut être modifiée depuis l'onglet « Paramètres > Dossiers »." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ini akan mengecualikan item yang dipilih dari pemindaian mendatang. Daftar pengecualian dapat diedit dari Pengaturan > tab Folder." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Questo escluderà gli elementi selezionati dalle scansioni future. La lista di esclusione può essere modificata da Impostazioni > Scheda Cartelle." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "選択した項目を今後のスキャンから除外します。除外リストは設定 > フォルダタブから編集できます。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이렇게 하면 선택한 항목이 향후 스캔에서 제외됩니다. 제외 목록은 설정 > 폴더 탭에서 편집할 수 있습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Spowoduje to wykluczenie wybranych elementów z przyszłych skanowań. Listę wykluczeń można edytować w zakładce Ustawienia > Foldery." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Isso excluirá os itens selecionados de futuras verificações. A lista de exclusão pode ser editada em Configurações > guia Pastas." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Isto excluirá os itens selecionados de futuras verificações. A lista de exclusão pode ser editada em Configurações > Aba Pastas." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Это исключит выбранные элементы из будущих сканирований. Список исключений можно изменить в настройках на вкладке Папки." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Týmto sa vylúčia vybrané položky z budúcich skenovaní. Zoznam vylúčení možno upraviť v Nastavenia > Karta Priečinky." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "To bo izključilo izbrane elemente iz prihodnjih pregledov. Seznam izključitev lahko urejate v Nastavitve > Zavihek Mape." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu, seçili öğeleri gelecekteki taramalardan hariç tutacaktır. Hariç tutma listesi Ayarlar > Klasörler sekmesinden düzenlenebilir." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Це виключить обрані елементи з майбутніх сканувань. Список Вийнятків можна відредагувати у Налаштування - вкладка Папки" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Điều này sẽ loại trừ các mục đã chọn khỏi các lần quét trong tương lai. Danh sách loại trừ có thể được chỉnh sửa từ Cài đặt > Tab Thư mục." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "这将使选定项在未来扫描中被排除。可以从“设置”>“文件夹”标签编辑排除列表。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "這會從未來的掃描中排除選擇的項目。可以透過「設定」>「資料夾」分頁編輯排除列表。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "這會從未來的掃描中排除選擇的項目。可以透過「設定」>「資料夾」分頁編輯排除列表。" } } } }, "This will install %@ from the tapped repository." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dies wird %@ aus dem ausgewählten Repository installieren." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Esto instalará %@ desde el repositorio seleccionado." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Cela installera %@ depuis le dépôt sélectionné." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ini akan menginstal %@ dari repositori yang diketuk." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Questo installerà %@ dal repository selezionato." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "これはタップされたリポジトリから%@をインストールします。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이것은 탭한 저장소에서 %@를 설치합니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "To zainstaluje %@ z wybranego repozytorium." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Isso instalará %@ do repositório selecionado." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Isto irá instalar %@ do repositório selecionado." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Это установит %@ из выбранного репозитория." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Toto nainštaluje %@ z vybraného úložiska." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "To bo namestilo %@ iz izbranega repozitorija." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu, dokunulan depodan %@ yükleyecek." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Це встановить %@ з вибраного репозиторію." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Điều này sẽ cài đặt %@ từ kho đã chọn." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "这将从选定的存储库安装%@。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "這將從點選的存儲庫安裝%@。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "這將從點選的存儲庫安裝%@。" } } } }, "This will install %@ using Homebrew. This may take several minutes." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dies wird %@ mit Homebrew installieren. Dies kann einige Minuten dauern." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Esto instalará %@ usando Homebrew. Esto puede tardar varios minutos." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Cela installera %@ en utilisant Homebrew. Cela peut prendre plusieurs minutes." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ini akan menginstal %@ menggunakan Homebrew. Ini mungkin memakan waktu beberapa menit." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Questo installerà %@ usando Homebrew. Questo potrebbe richiedere diversi minuti." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "これは Homebrew を使用して %@ をインストールします。これには数分かかる場合があります。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 작업은 Homebrew를 사용하여 %@를 설치합니다. 몇 분 정도 걸릴 수 있습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "To zainstaluje %@ za pomocą Homebrew. Może to potrwać kilka minut." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Isso instalará %@ usando o Homebrew. Isso pode levar vários minutos." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Isto irá instalar %@ usando o Homebrew. Isto pode levar vários minutos." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Это установит %@ с помощью Homebrew. Это может занять несколько минут." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Toto nainštaluje %@ pomocou Homebrew. Môže to trvať niekoľko minút." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "To bo namestilo %@ z uporabo Homebrew. To lahko traja nekaj minut." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu, Homebrew kullanarak %@ yükleyecek. Bu birkaç dakika sürebilir." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Це встановить %@ за допомогою Homebrew. Це може зайняти кілька хвилин." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Điều này sẽ cài đặt %@ bằng Homebrew. Quá trình này có thể mất vài phút." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "这将使用 Homebrew 安装 %@。这可能需要几分钟。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "這將使用 Homebrew 安裝 %@。這可能需要幾分鐘。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "這將使用 Homebrew 安裝 %@。這可能需要幾分鐘。" } } } }, "This will remove %@ from your system." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dies wird %@ von Ihrem System entfernen." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Esto eliminará %@ de su sistema." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Cela supprimera %@ de votre système." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ini akan menghapus %@ dari sistem Anda." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Questo rimuoverà %@ dal tuo sistema." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "これにより、%@ がシステムから削除されます。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 작업은 시스템에서 %@를 제거합니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "To usunie %@ z twojego systemu." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Isso removerá %@ do seu sistema." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Isto irá remover %@ do seu sistema." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Это удалит %@ из вашей системы." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Toto odstráni %@ z vášho systému." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "To bo odstranilo %@ iz vašega sistema." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu, %@'yi sisteminizden kaldıracaktır." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Це видалить %@ з вашої системи." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Điều này sẽ xóa %@ khỏi hệ thống của bạn." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "这将从您的系统中移除 %@。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "這將從您的系統中移除 %@。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "這將從您的系統中移除 %@。" } } } }, "This will remove all unused language translation files except your macOS language" : { "comment" : "Prune alert message", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dies wird alle ungenutzten Sprachübersetzungsdateien außer Ihrer macOS-Sprache entfernen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Esto eliminará todos los archivos de traducción de idiomas no utilizados, excepto el idioma de tu macOS" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Cela supprimera tous les fichiers de traduction de langue inutilisés sauf la langue de votre macOS" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ini akan menghapus semua file terjemahan bahasa yang tidak digunakan kecuali bahasa macOS Anda" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Questo rimuoverà tutti i file di traduzione delle lingue inutilizzati tranne la lingua del tuo macOS" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "これにより、macOS の言語を除くすべての未使用の言語翻訳ファイルが削除されます" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 작업은 macOS 언어를 제외한 모든 사용되지 않는 언어 번역 파일을 제거합니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "To usunie wszystkie nieużywane pliki tłumaczeń językowych z wyjątkiem języka Twojego macOS" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Isso removerá todos os arquivos de tradução de idiomas não utilizados, exceto o idioma do seu macOS" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Isso removerá todos os arquivos de tradução de idiomas não utilizados, exceto o idioma do seu macOS" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Это удалит все неиспользуемые файлы перевода языков, кроме языка вашей macOS" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Toto odstráni všetky nepoužívané súbory prekladov jazykov okrem jazyka vášho macOS" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "To bo odstranilo vse neuporabljene datoteke prevodov jezikov, razen jezika vašega macOS-a" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu, macOS diliniz hariç tüm kullanılmayan dil çeviri dosyalarını kaldıracaktır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Це видалить усі невикористані файли перекладів мов, окрім мови вашої macOS" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Điều này sẽ xóa tất cả các tệp dịch ngôn ngữ không sử dụng ngoại trừ ngôn ngữ macOS của bạn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "这将删除除您的 macOS 语言之外的所有未使用的语言翻译文件" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "這將刪除所有未使用的語言翻譯文件,除了您的 macOS 語言" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "這將刪除所有未使用的語言翻譯文件,除了您的 macOS 語言" } } } }, "This will upgrade %@ to the latest version using Homebrew. This may take several minutes." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dies wird %@ mit Homebrew auf die neueste Version aktualisieren. Dies kann einige Minuten dauern." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Esto actualizará %@ a la última versión usando Homebrew. Esto puede tardar varios minutos." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Cela mettra à jour %@ vers la dernière version en utilisant Homebrew. Cela peut prendre plusieurs minutes." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ini akan memperbarui %@ ke versi terbaru menggunakan Homebrew. Ini mungkin memerlukan beberapa menit." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Questo aggiornerà %@ all'ultima versione utilizzando Homebrew. Questo potrebbe richiedere diversi minuti." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "これは Homebrew を使用して %@ を最新バージョンにアップグレードします。これには数分かかる場合があります。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 작업은 Homebrew를 사용하여 %@을(를) 최신 버전으로 업그레이드합니다. 몇 분 정도 소요될 수 있습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "To zaktualizuje %@ do najnowszej wersji za pomocą Homebrew. Może to zająć kilka minut." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Isso atualizará %@ para a versão mais recente usando o Homebrew. Isso pode levar vários minutos." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Isto irá atualizar %@ para a versão mais recente usando o Homebrew. Isto pode demorar vários minutos." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Это обновит %@ до последней версии с помощью Homebrew. Это может занять несколько минут." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Toto aktualizuje %@ na najnovšiu verziu pomocou Homebrew. Môže to trvať niekoľko minút." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "To bo nadgradilo %@ na najnovejšo različico z uporabo Homebrew. To lahko traja nekaj minut." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu, Homebrew kullanarak %@'yı en son sürüme yükseltecektir. Bu birkaç dakika sürebilir." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Це оновить %@ до останньої версії за допомогою Homebrew. Це може зайняти кілька хвилин." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Điều này sẽ nâng cấp %@ lên phiên bản mới nhất bằng cách sử dụng Homebrew. Điều này có thể mất vài phút." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "这将使用 Homebrew 将 %@ 升级到最新版本。这可能需要几分钟。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "這將使用 Homebrew 將 %@ 升級到最新版本。這可能需要幾分鐘。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "這將使用 Homebrew 將 %@ 升級到最新版本。這可能需要幾分鐘。" } } } }, "This will:\n• Quit App Store and related processes\n• Clear download cache\n• Fix stuck or failed downloads\n\nYou may need to sign in again." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dies wird:\n• App Store und verwandte Prozesse beenden\n• Download-Cache leeren\n• Hängen gebliebene oder fehlgeschlagene Downloads reparieren\n\nMöglicherweise müssen Sie sich erneut anmelden." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Esto hará lo siguiente:\n• Cerrar App Store y procesos relacionados\n• Limpiar la caché de descargas\n• Solucionar descargas atascadas o fallidas\n\nEs posible que necesites iniciar sesión nuevamente." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Cela va :\n• Quitter l'App Store et les processus associés\n• Vider le cache de téléchargement\n• Réparer les téléchargements bloqués ou échoués\n\nVous devrez peut-être vous reconnecter." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ini akan:\n• Keluar dari App Store dan proses terkait\n• Membersihkan cache unduhan\n• Memperbaiki unduhan yang macet atau gagal\n\nAnda mungkin perlu masuk lagi." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Questo:\n• Uscirà da App Store e dai processi correlati\n• Cancellerà la cache dei download\n• Risolverà i download bloccati o falliti\n\nPotrebbe essere necessario accedere di nuovo." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "これにより、次のことが行われます:\n• App Store および関連プロセスを終了\n• ダウンロードキャッシュをクリア\n• 停止または失敗したダウンロードを修正\n\n再度サインインする必要があるかもしれません。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 작업은 다음을 수행합니다:\n• App Store 및 관련 프로세스를 종료합니다\n• 다운로드 캐시를 지웁니다\n• 멈추거나 실패한 다운로드를 수정합니다\n\n다시 로그인해야 할 수 있습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "To spowoduje:\n• Zakończenie działania App Store i powiązanych procesów\n• Wyczyścić pamięć podręczną pobierania\n• Naprawić zablokowane lub nieudane pobierania\n\nMoże być konieczne ponowne zalogowanie się." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Isso irá:\n• Fechar a App Store e processos relacionados\n• Limpar o cache de download\n• Corrigir downloads travados ou falhos\n\nPode ser necessário fazer login novamente." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Isto irá:\n• Sair da App Store e dos processos relacionados\n• Limpar a cache de downloads\n• Corrigir downloads bloqueados ou falhados\n\nPoderá precisar de iniciar sessão novamente." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Это приведет к следующим действиям:\n• Завершение работы App Store и связанных процессов\n• Очистка кэша загрузок\n• Исправление зависших или неудачных загрузок\n\nВозможно, вам потребуется снова войти в систему." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Toto vykoná:\n• Ukončí App Store a súvisiace procesy\n• Vymaže vyrovnávaciu pamäť sťahovania\n• Opraví zaseknuté alebo neúspešné sťahovania\n\nMožno sa budete musieť znova prihlásiť." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "To bo:\n• Zaprlo App Store in povezane procese\n• Počistilo predpomnilnik prenosa\n• Popravilo zataknjene ali neuspešne prenose\n\nMorda se boste morali znova prijaviti." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu işlem şunları yapacaktır:\n• App Store ve ilgili süreçleri kapat\n• İndirme önbelleğini temizle\n• Takılı veya başarısız indirmeleri düzelt\n\nTekrar oturum açmanız gerekebilir." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Це призведе до:\n• Завершення роботи App Store та пов'язаних процесів\n• Очищення кешу завантажень\n• Виправлення завислих або невдалих завантажень\n\nМожливо, вам знадобиться увійти знову." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Điều này sẽ:\n• Thoát App Store và các quy trình liên quan\n• Xóa bộ nhớ đệm tải xuống\n• Sửa lỗi tải xuống bị kẹt hoặc thất bại\n\nBạn có thể cần đăng nhập lại." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "这将会:\n• 退出 App Store 和相关进程\n• 清除下载缓存\n• 修复卡住或失败的下载\n\n您可能需要重新登录。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "這將會:\n• 退出 App Store 和相關進程\n• 清除下載快取\n• 修復卡住或失敗的下載\n\n您可能需要重新登入。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "這將會:\n• 退出 App Store 和相關進程\n• 清除下載快取\n• 修復卡住或失敗的下載\n\n您可能需要重新登入。" } } } }, "Toggle console output" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Konsolenausgabe umschalten" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Alternar salida de consola" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Basculer la sortie de la console" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Alihkan keluaran konsol" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Attiva/disattiva output della console" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "コンソール出力を切り替える" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "콘솔 출력 전환" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Przełącz wyjście konsoli" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Alternar saída do console" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Alternar saída do console" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Переключить вывод консоли" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Prepnúť výstup konzoly" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Preklopi izhod konzole" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Konsol çıkışını değiştir" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Перемкнути вивід консолі" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chuyển đổi đầu ra bảng điều khiển" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "切换控制台输出" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "切換控制台輸出" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "切換控制台輸出" } } } }, "Tools" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Werkzeug" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Herramientas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Outils" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Alat" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Strumenti" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ツール" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "도구" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Narzędzia" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ferramentas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ferramentas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Утилиты" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nástroje" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Orodja" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Araçlar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Інструменти" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Công cụ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "工具" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "工具" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "工具" } } } }, "Total Saved:" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Eingespart insgesamt:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Total guardado:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Gain total:" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Total Tersimpan:" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Totale Risparmiato:" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "合計節約量:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "총 저장됨:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Łącznie zaoszczędzono:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Total Economizado:" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Total Guardado:" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Итого сохранено:" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Ušetrené celkom:" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Skupaj shranjeno:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Toplam Tasarruf:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Загальна економія:" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tổng Đã Lưu:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "节省总数:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "節省總計:" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "總共釋放的儲存空間:" } } } }, "Total Size:" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Gesamtgröße:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Tamaño total:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Taille totale:" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ukuran Total:" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Dimensione Totale:" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "合計サイズ:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "총 크기:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Całkowity rozmiar:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Tamanho Total:" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Tamanho Total:" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Общий размер:" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Celková veľkosť:" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Skupna velikost:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Toplam Boyut:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Загальний розмір:" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tổng Kích thước:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "总大小:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "總大小:" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "總大小:" } } } }, "Transition animations disabled" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Übergangsanimationen deaktiviert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Animaciones de transición desactivadas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Animations de transition désactivé" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Animasi transisi dinonaktifkan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Animazioni di transizione disabilitate" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "移行アニメーションが無効化されました" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "전환 애니메이션 비활성화" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyłączone animacje przejścia " } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Animações de transição desativadas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Animações de transição desativadas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Анимации перехода отключены" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Animácie prechodov vypnuté" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Animacije prehoda onemogočene" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Geçiş animasyonları devre dışı bırakıldı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Анімація переходів вимкнена" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tắt hiệu ứng chuyển động" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "过渡动画已禁用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "已停用過渡動畫" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "轉場動畫已停用" } } } }, "Transition animations enabled" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Übergangsanimationen aktiviert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Animaciones de transición habilitadas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Animations de transition activé" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Animasi transisi diaktifkan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Animazioni di transizione abilitate" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "遷移アニメーションが有効" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "전환 애니메이션 활성화" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Włączone animacje przejścia " } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Animações de transição ativadas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Animações de transição ativadas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Анимация переходов включена" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Povolené prechodové animácie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prehodne animacije omogočene" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Geçiş animasyonları etkinleştirildi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Анімації переходів увімкнено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Bật hiệu ứng chuyển tiếp" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "启用过渡动画" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "已啟用過渡動畫" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "過渡動畫已啟用" } } } }, "Translation" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Übersetzung" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Traducción" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Traduction" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Terjemahan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Traduzione" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "翻訳" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "번역" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Tłumaczenie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Tradução" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Tradução" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Перевод" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Preklad" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prevajanje" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Çeviri" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Переклад" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Dịch thuật" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "翻译" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "翻譯" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "翻譯" } } } }, "Translations" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Übersetzungen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Traducciones" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Traductions" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Terjemahan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Traduzioni" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "翻訳" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "번역" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Tłumaczenia" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Traduções" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Traduções" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Переводы" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Preklady" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prevodi" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Çeviriler" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Переклади" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Bản dịch" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "翻译" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "翻譯" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "翻譯" } } } }, "Transparent material" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Durchscheinendes Material" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Material transparente" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Transparence" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Bahan transparan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Materiale trasparente" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "透明素材" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "투명 소재" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Przezroczysty materiał" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Material transparente" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Material transparente" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Прозрачный материал" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Priehľadnosť" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prosojen material" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Şeffaf materyal" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Прозорий матеріал" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chế độ trong suốt" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "透明质感" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "透明材料" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "透明材料" } } } }, "Transparent sidebar" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Transparente Seitenleiste" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Barra lateral transparente" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Barre latérale transparente" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Bilah samping transparan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Barra laterale trasparente" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "透過サイドバー" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "투명 사이드바" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Przezroczysty panel boczny" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Barra lateral transparente" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Barra lateral transparente" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Прозрачная боковая панель" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Priehľadný bočný panel" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prosojen stranski meni" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Saydam yan panel" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Прозора бічна панель" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thanh bên trong suốt" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "透明侧边栏" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "透明側欄" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "透明側邊欄" } } } }, "Try a different search term" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Versuchen Sie einen anderen Suchbegriff" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Prueba con otro término de búsqueda" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Essayez un autre terme de recherche" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Coba istilah pencarian lain" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Prova un termine di ricerca diverso" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "別の検索語を試してください" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "다른 검색어를 시도해보세요" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Spróbuj innego terminu wyszukiwania" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Tente um termo de pesquisa diferente" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Tente um termo de pesquisa diferente" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Попробуйте другой поисковый запрос" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Skúste iný vyhľadávací výraz" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Poskusite z drugačnim iskalnim izrazom" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Farklı bir arama terimi deneyin" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Спробуйте інший пошуковий запит" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thử một thuật ngữ tìm kiếm khác" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "尝试不同的搜索词" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "嘗試不同的搜尋詞" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "嘗試不同的搜尋詞" } } } }, "Try adjusting your filters" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Versuchen Sie, Ihre Filter anzupassen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Intenta ajustar tus filtros" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Essayez d'ajuster vos filtres" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Coba sesuaikan filter Anda" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Prova a regolare i tuoi filtri" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "フィルターを調整してみてください" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "필터를 조정해 보세요" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Spróbuj dostosować filtry" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Tente ajustar seus filtros" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Tente ajustar os seus filtros" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Попробуйте изменить фильтры" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Skúste upraviť svoje filtre" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Poskusite prilagoditi filtre" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Filtrelerinizi ayarlamayı deneyin" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Спробуйте змінити фільтри" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thử điều chỉnh bộ lọc của bạn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "尝试调整您的过滤器" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "嘗試調整您的篩選器" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "嘗試調整您的篩選器" } } } }, "Try adjusting your search or filters" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Versuchen Sie, Ihre Suche oder Filter anzupassen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Intenta ajustar tu búsqueda o filtros" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Essayez d'ajuster votre recherche ou vos filtres" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Coba sesuaikan pencarian atau filter Anda" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Prova a modificare la tua ricerca o i filtri" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "検索条件やフィルターを調整してみてください" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "검색이나 필터를 조정해 보세요" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Spróbuj dostosować swoje wyszukiwanie lub filtry" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Tente ajustar sua pesquisa ou filtros" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Tente ajustar a sua pesquisa ou filtros" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Попробуйте изменить условия поиска или фильтры" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Skúste upraviť vyhľadávanie alebo filtre" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Poskusite prilagoditi iskanje ali filtre" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Aramanızı veya filtrelerinizi değiştirmeyi deneyin" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Спробуйте змінити свій пошук або фільтри" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hãy thử điều chỉnh tìm kiếm hoặc bộ lọc của bạn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "尝试调整您的搜索或过滤器" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "嘗試調整您的搜索或篩選條件" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "嘗試調整您的搜尋或篩選條件" } } } }, "Type" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Typ" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Tipo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Type" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Jenis" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Tipo" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "タイプ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "유형" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Typ" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Tipo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Tipo" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Тип" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Typ" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Vrsta" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Tür" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Тип" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Loại" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "类型" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "類型" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "類型" } } } }, "Type a keyword to exclude, Enter ↵ to save" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Geben Sie ein zu ausschliessendes Stichwort ein und drücken Sie die Zeilenschaltung, um zu speichern." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Escribe una palabra clave para excluir, presiona Enter ↵ para guardar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Saisissez un mot-clé pour exclusion, Entrée ↵ pour sauvegarder" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ketik kata kunci untuk dikecualikan, Tekan Enter ↵ untuk menyimpan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Digita una parola chiave da escludere, premi Invio ↵ per salvare" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "除外するキーワードを入力し、Enter ↵ キーを押して保存" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "제외할 키워드를 입력하고, 저장하려면 Enter ↵를 누르세요" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wpisz słowo kluczowe, które chcesz wykluczyć, a następnie naciśnij Enter ↵, aby zapisać" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Digite uma palavra-chave para excluir, pressione Enter ↵ para salvar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Digite uma palavra-chave para excluir, pressione Enter ↵ para salvar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Введите ключевое слово для исключения, нажмите Enter ↵ для сохранения" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zadajte kľúčové slovo na vylúčenie, Enter ↵ pre uloženie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Vnesite ključno besedo za izključitev, pritisnite Enter ↵ za shranjevanje" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hariç tutulacak bir anahtar kelime yazın, kaydetmek için Enter ↵ tuşuna basın" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Введіть ключове слово для виключення, натисніть Enter ↵, щоб зберегти" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Nhập từ khóa để loại trừ, nhấn Enter ↵ để lưu" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "输入一个关键词以排除,按回车键 ↵ 保存" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "輸入關鍵詞以排除,按一下 Enter (↵) 以儲存" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "輸入關鍵詞以排除,按一下 Enter (↵) 以儲存" } } } }, "Undo Removal" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Entfernung rückgängig machen " } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Deshacer eliminación" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Annuler la suppression" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Batalkan Penghapusan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Annulla rimozione" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "削除を元に戻す" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "제거 돌리기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Cofnij usunięcie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Desfazer Remoção" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Desfazer Remoção" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Отменить удаление" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zrušiť odstránenie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Razveljavi odstranitev" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kaldırmayı Geri Al" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Скасувати видалення" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hoàn tác việc xóa" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "撤销删除" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "還原移除" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "還原移除" } } } }, "Unhide update" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Update einblenden" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar actualización" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Afficher la mise à jour" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tampilkan pembaruan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Mostra aggiornamento" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "更新を表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "업데이트 숨김 해제" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Pokaż aktualizację" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar atualização" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Mostrar atualização" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Показать обновление" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť aktualizáciu" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Prikaži posodobitev" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Güncellemeyi göster" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Показати оновлення" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hiện cập nhật" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "显示更新" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "取消隱藏更新" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "取消隱藏更新" } } } }, "Uninstall" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Deinstallieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Désinstaller" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Copot pemasangan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Disinstalla" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アンインストール" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "제거" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Odinstaluj" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odinštalovať" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odstrani" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kaldır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Gỡ cài đặt" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "卸载" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "解除安裝" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "解除安裝" } } } }, "Uninstall %@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Deinstallieren %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalar %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Désinstaller %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Copot pemasangan %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Disinstalla %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アンインストール %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@ 제거" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Odinstaluj %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalar %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalar %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odinštalovať %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odstrani %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kaldır %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Gỡ cài đặt %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "卸载 %@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "卸載 %@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "解除安裝 %@" } } } }, "Uninstall %@?" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Deinstalliere %@?" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "¿Desinstalar %@?" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Désinstaller %@?" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus pemasangan %@?" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Disinstallare %@?" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%@ をアンインストールしますか?" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@를 제거하시겠습니까?" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Odinstalować %@?" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalar %@?" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalar %@?" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить %@?" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odinštalovať %@?" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odstraniti %@?" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%@ kaldırılacak mı?" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити %@?" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Gỡ cài đặt %@?" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "卸载 %@?" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "卸載 %@?" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "解除安裝 %@?" } } } }, "Uninstall All" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Alle deinstallieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalar todo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Désinstaller tout" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Copotkan Semua" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Disinstalla tutto" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "すべてアンインストール" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "모두 제거" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Odinstaluj wszystko" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalar Tudo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalar Tudo" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить все" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odinštalovať všetko" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odstrani vse" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hepsini Kaldır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити все" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Gỡ cài đặt tất cả" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "卸载全部" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "卸載全部" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "全部卸載" } } } }, "Uninstall confirmation alerts" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Deinstallations-Warnung " } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Alertas de confirmación de desinstalación" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Alertes de confirmation de désinstallation" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Peringatan konfirmasi pencopotan pemasangan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Avvisi di conferma disinstallazione" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アンインストール確認アラート" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "제거 확인 알림" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Komunikat potwierdzający odinstalowanie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Alertas de confirmação de desinstalação" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Alertas de confirmação de desinstalação" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Предупреждения о подтверждении удаления" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Potvrdzovacie hlásenia pri odinštalácii" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Opozorila za potrditev odstranitve" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kaldırma onay uyarıları" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Сповіщення про підтвердження видалення" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xác nhận gỡ cài đặt" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "卸载确认警报" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "解除安裝確認提示" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "解除安裝確認提示" } } } }, "Uninstall package" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Paket deinstallieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalar paquete" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Désinstaller le paquet" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Copot pemasangan paket" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Disinstalla pacchetto" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "パッケージをアンインストール" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "패키지 제거" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Odinstaluj pakiet" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalar pacote" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalar pacote" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить пакет" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odinštalovať balík" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odstrani paket" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Paketi kaldır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити пакет" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Gỡ cài đặt gói" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "卸载软件包" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "卸載套件" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "解除安裝套件" } } } }, "Uninstall Pearcleaner" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner deinstallieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalar Pearcleaner" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Désinstaller Pearcleaner" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Copot Pearcleaner" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Disinstalla Pearcleaner" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleanerをアンインストール" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner 제거" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Odinstaluj Pearcleaner" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalar Pearcleaner" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalar Pearcleaner" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить Pearcleaner" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odinštalovať Pearcleaner" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odstrani Pearcleaner" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Pearcleaner’ı Kaldır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити Pearcleaner" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Gỡ cài đặt Pearcleaner" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "卸载 Pearcleaner" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "解除安裝 Pearcleaner" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "解除安裝 Pearcleaner" } } } }, "Uninstall Selected (%lld)" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ausgewählte deinstallieren (%lld)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalar seleccionado (%lld)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Désinstaller la sélection (%lld)" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Hapus Instalasi yang Dipilih (%lld)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Disinstalla selezionato (%lld)" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "選択したものをアンインストール (%lld)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "선택 항목 제거 (%lld)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Odinstaluj wybrane (%lld)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalar Selecionado (%lld)" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalar Selecionado (%lld)" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удалить выбранные (%lld)" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odinštalovať vybrané (%lld)" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odstrani izbrano (%lld)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Seçiliyi Kaldır (%lld)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалити вибране (%lld)" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Gỡ cài đặt đã chọn (%lld)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "卸载所选 (%lld)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "解除安裝選取項目 (%lld)" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "解除安裝所選 (%lld)" } } } }, "Uninstalling..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Deinstallation..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalando..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Désinstallation..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Mencopot…" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Disinstallazione in corso..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アンインストール中..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "제거 중..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Odinstalowywanie..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalando..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Desinstalando..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Удаление..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odinštalovávanie..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odstranjevanje..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kaldırılıyor..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видалення..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang gỡ cài đặt..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "正在卸载..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "正在卸載..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "正在卸載..." } } } }, "Unit" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Einheit" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Unidad" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Unité" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Unit" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Unità" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "単位" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "단위" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Jednostka" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Unidade" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Unidade" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Единица" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Jednotka" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Enota" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Birim" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Одиниця" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đơn vị" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "单位" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "單位" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "單位" } } } }, "universal" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "universal" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "universal" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "universel" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "universal" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "universale" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ユニバーサル" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Universal" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "uniwersalny" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "universal" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "universal" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "универсальный" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "univerzálny" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "univerzalno" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "evrensel" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "універсальний" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "phổ quát" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "通用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "通用" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "通用" } } } }, "Unknown" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Unbekannt" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Desconocido" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Inconnu" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak diketahui" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Sconosciuto" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "不明" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "알 수 없음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nieznany" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Desconhecido" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Desconhecido" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Неизвестно" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Neznáme" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Neznano" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bilinmeyen" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Невідомо" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không xác định" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "未知" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "未知" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "未知" } } } }, "Unknown service status (%lld)." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Unbekannter Dienst Zustand (%lld)." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Estado del servicio desconocido (%lld)." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Statut de service inconnu (%lld)." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Status layanan tidak diketahui (%lld)." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Stato del servizio sconosciuto (%lld)." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "不明なサービスステータス(%lld)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "알 수 없는 서비스 상태 (%lld)." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nieznany stan usługi (%lld)." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Status do serviço desconhecido (%lld)." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Estado de serviço desconhecido (%lld)." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Неизвестный статус службы (%lld)" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Neznámy stav služby (%lld)." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Neznano stanje storitve (%lld)." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bilinmeyen servis durumu (%lld)." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Невідомий статус служби (%lld)" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Trạng thái dịch vụ không xác định (%lld)." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "未知服务状态(%lld)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "未知的服務狀態(%lld)。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "未知的服務狀態(%lld)。" } } } }, "Unlink File" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dateiverknüpfung aufheben" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Desvincular archivo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Dissocier le fichier" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Putuskan Tautan File" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Scollega file" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ファイルのリンクを解除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "파일 연결 해제" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Odłącz plik" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Desvincular Arquivo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Desvincular Ficheiro" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Отвязать файл" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odpojiť súbor" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odstrani povezavo datoteke" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Dosya Bağlantısını Kaldır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Від'єднати файл" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hủy liên kết tệp" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "解除文件链接" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "取消連結檔案" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "取消連結檔案" } } } }, "Unload launch daemons and agents" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Start-Daemons und -Agenten entladen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Descargar demonios y agentes de lanzamiento" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Décharger les démons et agents de lancement" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lepaskan daemon dan agen peluncuran" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Scarica i demoni e gli agenti di avvio" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "起動デーモンとエージェントをアンロード" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "런치 데몬 및 에이전트 언로드" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyładuj demony i agenty uruchamiania" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Descarregar daemons e agentes de inicialização" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Descarregar daemons e agentes de lançamento" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Выгрузить демоны и агенты запуска" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vyložiť spúšťacie démony a agentov" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Raztovorite zagonske demone in agente" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Başlatma daemonlarını ve ajanlarını boşalt" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Вивантажити демонів і агентів запуску" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Dỡ bỏ các daemon và tác nhân khởi động" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "卸载启动守护程序和代理" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "卸載啟動守護程序和代理" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "卸載啟動守護程序和代理" } } } }, "Unload the service" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Dienst entladen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Descargar el servicio" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Décharger le service" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lepaskan layanan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Scarica il servizio" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "サービスをアンロード" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "서비스 언로드" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zatrzymaj usługę" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Descarregar o serviço" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Descarregar o serviço" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Выгрузить службу" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Ukončiť službu" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Raztovori storitev" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Servisi devre dışı bırak" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Вивантажити сервіс" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Dỡ bỏ dịch vụ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "卸载服务" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "解除服務" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "卸載該服務" } } } }, "Unpin version" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Version lösen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Desanclar versión" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Détacher la version" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lepaskan pin versi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Sblocca versione" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "バージョンの固定を解除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "버전 고정 해제" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Odepnij wersję" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Desafixar versão" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Desafixar versão" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Открепить версию" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Odopnúť verziu" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odpni različico" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sürümü sabitlemeyi kaldır" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Відкріпити версію" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Bỏ ghim phiên bản" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "取消固定版本" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "取消釘選版本" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "取消釘選版本" } } } }, "Unregister Service" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Nicht registrierter Dienst" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cancelar registro de servicio" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Désinscrire le service" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Batalkan Pendaftaran Layanan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Deregistra servizio" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "サービスの登録解除" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "서비스 등록 취소" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyrejestruj usługę" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Cancelar Registro do Serviço" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Cancelar registo do serviço" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Отменить регистрацию службы" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zrušiť registráciu služby" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Odregistriraj storitev" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Servisin Kaydını Sil" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Скасувати Реєстрацію Сервіса" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hủy đăng ký dịch vụ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "注销服务" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "取消註冊服務" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "取消註冊服務" } } } }, "Unsigned" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Nicht signiert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Sin firmar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Non signé" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Belum ditandatangani" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Non firmato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "未署名" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "서명되지 않음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Niepodpisany" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Não assinado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Não assinado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Без подписи" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nepodpísaný" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Nepodpisano" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "İmzalanmamış" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Без підпису" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Chưa ký" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "未签名" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "未簽名" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "未簽名" } } } }, "Unsupported" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Nicht unterstützt" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "No compatible" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Non pris en charge" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tidak didukung" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Non supportato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "サポートされていません" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "지원되지 않음" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Nieobsługiwane" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Não suportado" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Não suportado" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Не поддерживается" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Nepodporované" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ni podprto" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Desteklenmiyor" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Не підтримується" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Không được hỗ trợ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "不支持" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "不支援" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "不支持" } } } }, "Update" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Aktualisieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualizar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mise à jour" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Perbarui" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiorna" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "更新" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "업데이트" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizacja" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Обновить" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizovať" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Posodobi" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Güncelleme" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оновити" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Cập nhật" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "更新" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "更新" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "更新" } } } }, "Update (Pinned)" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Aktualisieren (Angeheftet)" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualizar (Fijado)" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mettre à jour (Épinglé)" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Perbarui (Disematkan)" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiorna (Fissato)" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "更新(ピン留め)" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "업데이트 (고정됨)" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizuj (Przypięte)" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar (Fixado)" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Atualização (Fixado)" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Обновить (Закреплено)" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizovať (Pripnuté)" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Posodobi (Pripeto)" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Güncelle (Sabitlenmiş)" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оновити (Закріплено)" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Cập nhật (Đã ghim)" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "更新(置顶)" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "更新(已固定)" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "更新(已固定)" } } } }, "Update %@?" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Aktualisieren %@?" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "¿Actualizar %@?" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mettre à jour %@?" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Perbarui %@?" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiornare %@?" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%@を更新しますか?" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@의 업데이트를 하시겠습니까?" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zaktualizować %@?" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar %@?" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar %@?" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Обновить %@?" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizovať %@?" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Posodobiti %@?" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%@ güncellensin mi?" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оновити %@?" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Cập nhật %@?" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "更新 %@?" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "更新 %@?" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "更新 %@?" } } } }, "Update All" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Alle aktualisieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualizar todo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Tout mettre à jour" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Perbarui Semua" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiorna Tutto" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "すべて更新" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "모두 업데이트" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zaktualizuj wszystko" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar tudo" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar Tudo" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Обновить все" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizovať všetko" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Posodobi vse" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hepsini Güncelle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оновити все" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Cập nhật tất cả" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "全部更新" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "全部更新" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "全部更新" } } } }, "Update all available apps" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Alle verfügbaren Apps aktualisieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualizar todas las aplicaciones disponibles" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mettre à jour toutes les applications disponibles" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Perbarui semua aplikasi yang tersedia" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiorna tutte le app disponibili" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "利用可能なすべてのアプリを更新" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "모든 사용 가능한 앱 업데이트" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zaktualizuj wszystkie dostępne aplikacje" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar todos os aplicativos disponíveis" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar todas as aplicações disponíveis" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Обновить все доступные приложения" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizovať všetky dostupné aplikácie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Posodobi vse razpoložljive aplikacije" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Tüm mevcut uygulamaları güncelle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оновити всі доступні додатки" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Cập nhật tất cả ứng dụng có sẵn" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "更新所有可用的应用程序" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "更新所有可用的應用程式" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "更新所有可用的應用程式" } } } }, "Update Anyway" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Trotzdem aktualisieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualizar de todos modos" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mettre à jour quand même" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tetap Perbarui" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiorna comunque" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "それでも更新" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "어쨌든 업데이트" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zaktualizuj mimo to" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar Mesmo Assim" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar Mesmo Assim" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Обновить в любом случае" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizovať aj tak" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Posodobi vseeno" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yine de Güncelle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оновити все одно" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Cập nhật Dù sao" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "仍然更新" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "仍然更新" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "仍然更新" } } } }, "Update Available" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Update verfügbar" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualización Disponible" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mise à jour disponible" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pembaruan Tersedia" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiornamento disponibile" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "更新が可能です" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "업데이트 가능" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dostępna aktualizacja" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualização Disponível" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Atualização Disponível" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Доступно обновление" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizácia Dostupná" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Na voljo je posodobitev" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Güncelleme Mevcut" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оновлення Доступне" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Có bản cập nhật" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "有可用更新" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "有可用的更新" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "有可用的更新" } } } }, "Update Available 🥳" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Update verfügbar 🥳" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualización disponible 🥳" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Une mise à jour est disponible 🥳" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pembaruan Tersedia 🥳" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiornamento Disponibile 🥳" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "更新が可能です 🥳" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "업데이트 가능 🥳" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Dostępna aktualizacja 🥳" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualização Disponível 🥳" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Atualização Disponível 🥳" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Доступно обновление 🥳" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizácia Dostupná 🥳" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Posodobitev na voljo 🥳" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Güncelleme Mevcut 🥳" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оновлення Доступне 🥳" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Có bản cập nhật 🥳" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "有可用更新 🥳" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "有可用的更新 🥳" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "有可用的更新 🥳" } } } }, "Update Available!" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Update verfügbar!" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "¡Actualización disponible!" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mise à jour disponible !" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pembaruan Tersedia!" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiornamento disponibile!" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アップデートがあります!" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "업데이트 가능!" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizacja dostępna!" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualização disponível!" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Atualização Disponível!" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Доступно обновление!" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizácia k dispozícii!" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Posodobitev na voljo!" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Güncelleme Mevcut!" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Доступне оновлення!" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Cập nhật có sẵn!" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "更新可用!" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "更新可用!" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "更新可用!" } } } }, "Update completed" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Updates vollständig" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualización completada" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mise à jour terminée" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pembaruan selesai" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiornamento completato" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "更新完了" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "업데이트 완료" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizacja zakończona" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualização concluída" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Atualização concluída" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Обновление завершено" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizácia dokončená" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Posodobitev dokončana" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Güncelleme tamamlandı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оновлення виконано" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đã hoàn tất cập nhật" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "更新完成" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "更新完成" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "更新完成" } } } }, "Update Frequency" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Updatefrequenz" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Frecuencia de actualización" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Recherche de mise à jour" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Frekuensi Pembaruan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Frequenza di aggiornamento" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "更新頻度" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "업데이트 주기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Częstotliwość aktualizacji" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Frequência de Atualização" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Frequência de Atualização" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Частота обновления" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Frekvencia aktualizácií" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pogostost posodobitev" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Güncelleme Sıklığı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Частота Оновлень" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Tần suất cập nhật" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "更新频率" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "更新頻率" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "更新頻率" } } } }, "Update Homebrew" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew aktualisieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualizar Homebrew" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mettre à jour Homebrew" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Perbarui Homebrew" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiorna Homebrew" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Homebrewを更新" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 업데이트" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zaktualizuj Homebrew" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar Homebrew" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar Homebrew" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Обновить Homebrew" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizovať Homebrew" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Posodobi Homebrew" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew'u Güncelle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оновити Homebrew" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Cập nhật Homebrew" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "更新 Homebrew" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "更新 Homebrew" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "更新 Homebrew" } } } }, "Update Homebrew itself" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew selbst aktualisieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualizar Homebrew" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mettre à jour Homebrew lui-même" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Perbarui Homebrew itu sendiri" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiorna Homebrew" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew自体を更新する" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 자체 업데이트" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zaktualizuj Homebrew" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar o próprio Homebrew" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar o próprio Homebrew" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Обновить Homebrew" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizovať Homebrew" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Posodobi Homebrew" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew'ü güncelle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оновити Homebrew" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Cập nhật Homebrew" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "更新 Homebrew 本身" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "更新 Homebrew 本身" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "更新 Homebrew 本身" } } } }, "Update Homebrew to the latest version using brew update" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Aktualisieren Sie Homebrew auf die neueste Version mit brew update" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualiza Homebrew a la última versión usando brew update" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mettez à jour Homebrew vers la dernière version en utilisant brew update" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Perbarui Homebrew ke versi terbaru menggunakan brew update" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiorna Homebrew all'ultima versione usando brew update" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "brew update を使用して Homebrew を最新バージョンに更新する" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "brew update를 사용하여 Homebrew를 최신 버전으로 업데이트하십시오" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zaktualizuj Homebrew do najnowszej wersji za pomocą brew update" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualize o Homebrew para a versão mais recente usando brew update" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Atualize o Homebrew para a versão mais recente usando brew update" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Обновите Homebrew до последней версии с помощью brew update" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizujte Homebrew na najnovšiu verziu pomocou brew update" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Posodobite Homebrew na najnovejšo različico z uporabo brew update" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew'u en son sürüme brew update kullanarak güncelleyin" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оновіть Homebrew до останньої версії за допомогою brew update" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Cập nhật Homebrew lên phiên bản mới nhất bằng cách sử dụng brew update" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "使用 brew update 将 Homebrew 更新到最新版本" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "使用 brew update 將 Homebrew 更新到最新版本" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "使用 brew update 將 Homebrew 更新到最新版本" } } } }, "Update in App Store" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Im App Store aktualisieren" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualizar en App Store" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mettre à jour dans l'App Store" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Perbarui di App Store" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiorna nell'App Store" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "App Storeで更新" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "App Store 에서 업데이트" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zaktualizuj w App Store" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar na App Store" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar na App Store" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Обновить в App Store" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizovať v App Store" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Posodobi v App Store" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "App Store'da Güncelle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оновити в App Store" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Cập nhật trong App Store" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "在 App Store 更新" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "在 App Store 更新" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "在 App Store 更新" } } } }, "Update in progress" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Update wird durchgeführt" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualización en curso" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mise à jour en cours" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pembaruan sedang berlangsung" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiornamento in corso" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "更新中" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "업데이트 진행 중" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizacja w toku" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualização em andamento" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Atualização em progresso" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Обновление в процессе" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Prebieha aktualizácia" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Posodobitev v teku" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Güncelleme devam ediyor" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оновлення в процесі" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang cập nhật" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "更新进行中" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "更新正在進行" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "更新正在進行" } } } }, "Updated %@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Aktualisiert am %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualizado %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mis à jour %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Diperbarui %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiornato %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "更新日 %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "업데이트됨 %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zaktualizowano %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualizado %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Atualizado %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Обновлено %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizované %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Posodobljeno %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%@ güncellendi" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оновлено %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đã cập nhật %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "更新于%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "已更新 %@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "更新於 %@" } } } }, "Updater" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Aktualisierer" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualizador" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mise à jour" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pembaruan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiornamento" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アップデーター" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "업데이터" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizator" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualizador" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Atualizador" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Обновление" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizátor" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Posodobitelj" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Güncelleyici" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оновлювач" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Trình cập nhật" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "更新程序" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "更新程式" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "更新程式" } } } }, "Updater frequency is set to Never" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Updatefrequenz ist auf nie gesetzt" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "La frecuencia del actualizador está configurada en Nunca" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "La fréquence de mise à jour est définie sur Jamais" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Frekuensi pembaruan diatur ke Tidak Pernah" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "La frequenza di aggiornamento è impostata su Mai" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アップデーターの頻度が「なし」に設定されています" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "업데이트 확인을 '안 함'으로 설정됨" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Częstotliwość aktualizacji jest ustawiona na Nigdy" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "A frequência de atualização está definida como Nunca" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A frequência do atualizador está definida como Nunca" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Частота обновления установлена на значение Никогда" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Frekvencia aktualizácií je nastavená na Nikdy" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pogostost posodobitev je nastavljena na Nikoli" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Güncelleme sıklığı Hiçbir Zaman olarak ayarlandı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Автоматичні оновлення вимкнено" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đã thiết lập không bao giờ kiểm tra cập nhật" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "更新频率已设置为从不" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "更新頻率已設定為「永不」" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "更新頻率已設定為「永不」" } } } }, "Updates Disabled" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Updates ausgeschaltet" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualizaciones desactivadas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mises à jour désactivées" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pembaruan Dinonaktifkan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiornamenti disabilitati" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "更新が無効になっています" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "업데이트 비활성화됨" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizacje wyłączone" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualizações Desativadas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Atualizações Desativadas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Обновления отключены" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizácie vypnuté" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Posodobitve onemogočene" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Güncellemeler Devre Dışı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оновлення вимкнені" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đã tắt cập nhật" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "更新已禁用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "更新已停用" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "更新已停用" } } } }, "Updating apps..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Apps werden aktualisiert..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualizando aplicaciones..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mise à jour des applications..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Memperbarui aplikasi..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiornamento delle app..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アプリを更新しています..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "앱 업데이트 중..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizowanie aplikacji..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualizando aplicativos..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A atualizar aplicações..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Обновление приложений..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizácia aplikácií..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Posodabljanje aplikacij..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uygulamalar güncelleniyor..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оновлення додатків..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang cập nhật ứng dụng..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "正在更新应用程序..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "正在更新應用程式..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "正在更新應用程式..." } } } }, "Updating..." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Aktualisierung..." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualizando..." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mise à jour..." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Memperbarui..." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiornamento in corso..." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "更新中..." } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "업데이트 중..." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizacja..." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualizando..." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Atualizando..." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Обновление..." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizácia..." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Posodabljanje..." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Güncelleniyor..." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оновлення..." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang cập nhật..." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "正在更新..." } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "正在更新..." } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "正在更新..." } } } }, "Upgrade currently installed packages" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Aktualisieren Sie die derzeit installierten Pakete" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualizar los paquetes instalados actualmente" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Mettre à jour les paquets actuellement installés" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tingkatkan paket yang saat ini terpasang" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Aggiorna i pacchetti attualmente installati" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "現在インストールされているパッケージをアップグレード" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "현재 설치된 패키지 업그레이드" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Uaktualnij obecnie zainstalowane pakiety" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar pacotes instalados atualmente" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar pacotes atualmente instalados" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Обновить установленные пакеты" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Aktualizovať aktuálne nainštalované balíky" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Nadgradite trenutno nameščene pakete" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Şu anda yüklü olan paketleri yükselt" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Оновити встановлені пакети" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Nâng cấp các gói hiện đang cài đặt" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "升级当前安装的软件包" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "升級當前安裝的軟體包" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "升級目前安裝的套件" } } } }, "Upgrade Packages" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Upgrade-Pakete" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Actualizar paquetes" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Forfaits de mise à niveau" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Paket Peningkatan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Pacchetti di aggiornamento" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "パッケージをアップグレード" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "업그레이드 패키지" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zaktualizuj pakiety" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar pacotes" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Atualizar Pacotes" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Обновить пакеты" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Balíky aktualizácií" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Nadgradi pakete" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yükseltme Paketleri" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Пакети оновлення" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Gói Nâng Cấp" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "升级包" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "升級包" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "升級套件" } } } }, "User" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Benutzer" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Usuario" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Utilisateur" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pengguna" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Utente" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ユーザー" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "사용자" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Użytkownik" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Usuário" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Utilizador" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Пользователь" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Používateľ" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Uporabnik" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kullanıcı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Користувач" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Người dùng" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "用户" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "用户" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "用户" } } } }, "User denied permission. Enable in System Settings > Login Items." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Benutzer hat die Berechtigung verweigert. Aktivieren Sie diese Option in den Systemeinstellungen > Anmeldeobjekte." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "El usuario denegó el permiso. Habilite en Configuración del sistema > Elementos de inicio de sesión." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Autorisation refusée à l'utilisateur. Activer dans Paramètres système > Éléments de connexion." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pengguna menolak izin. Aktifkan di Pengaturan Sistem > Item Login." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Permesso negato dall'utente. Abilita in Impostazioni di Sistema > Elementi di Login." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ユーザーが許可を拒否しました。システム設定 > ログイン項目で有効にしてください" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "사용자가 권한을 거부했습니다. 시스템 설정 > 로그인 항목에서 활성화하세요." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Użytkownik odmówił zgody. Włącz w Ustawieniach systemowych > Rzeczy i rozszerzenia otwierane podczas logowania." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Usuário negou permissão. Ative em Configurações do Sistema > Itens de Login." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Permissão negada pelo utilizador. Ativar em Definições do Sistema > Itens de Início de Sessão." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Пользователь отказал в разрешении. Включите в Настройки системы > Элементы входа." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Používateľ odmietol povolenie. Umožnite v Systémových nastaveniach > Položky pri prihlásení." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Uporabnik je zavrnil dovoljenje. Omogočite v Sistemske nastavitve > Prijavni elementi." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Kullanıcı izni reddetti. Sistem Ayarları > Oturum Açma Öğeleri'nde etkinleştirin." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Користувач відмовив у дозволі. Увімкніть у Системних налаштуваннях > Елементи входу." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Người dùng đã từ chối cấp quyền. Vui lòng bật trong Cài đặt hệ thống > Mục đăng nhập." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "用户拒绝了权限。请在系统设置 > 登录项中启用。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "用户拒絕了權限。在「系統設定」>「登入項目」中啟󠄁用。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "用户拒絕了權限。在「系統設定」>「登入項目」中啟󠄁用。" } } } }, "user/tap" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "user/tap" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "user/tap" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "user/tap" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "user/tap" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "user/tap" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "user/tap" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "user/tap" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "user/tap" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "user/tap" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "user/tap" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "user/tap" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "user/tap" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "user/tap" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "user/tap" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "user/tap" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "user/tap" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "user/tap" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "user/tap" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "user/tap" } } } }, "Uses from macOS" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Verwendungen von macOS" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Usos de macOS" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Utilisations de macOS" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Penggunaan dari macOS" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Utilizzi da macOS" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "macOS からの使用" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "macOS에서 사용" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Użycia z macOS" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Usos do macOS" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Usos do macOS" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Использования из macOS" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Použitia z macOS" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Uporablja iz macOS" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "macOS'tan Kullanımlar" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Використання з macOS" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Sử dụng từ macOS" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "来自 macOS 的使用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "來自 macOS 的使用" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "來自 macOS 的使用" } } } }, "Valid cask: %@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Gültiges Fass: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Barril válido: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Fût valide : %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Tong yang valid: %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Fusto valido: %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "有効な樽: %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "유효한 통: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Prawidłowa beczka: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Barril válido: %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Barril válido: %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Действительная бочка: %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Platný sud: %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Veljavna sod: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Geçerli fıçı: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Дійсна бочка: %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thùng hợp lệ: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "有效桶:%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "有效的桶: %@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "有效的桶: %@" } } } }, "Value" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Wert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Valor" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Valeur" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Nilai" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Valore" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "値" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "값" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wartość" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Valor" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Valor" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Значение" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Hodnota" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Vrednost" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Değer" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Значення" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Giá trị" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "值" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "值" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "值" } } } }, "Version %@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Version %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Versión %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Version %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Versi %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Versione %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "バージョン %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "버전 %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wersja %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Versão %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Versão %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Версия %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Verzia %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Različica %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Versiyon %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Версія %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Phiên bản %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "版本 %@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "版本 %@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "版本 %@" } } } }, "Version:" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Version:" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Versión:" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Version :" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Versi:" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Versione:" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "バージョン:" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "버전:" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wersja:" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Versão:" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Versão:" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Версия:" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Verzia:" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Različica:" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sürüm:" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Версія:" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Phiên bản:" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "版本:" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "版本:" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "版本:" } } } }, "Version: %@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Version: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Versión: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Version : %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Versi: %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Versione: %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "バージョン: %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "버전: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wersja %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Versão: %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Versão: %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Версия: %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Verzia: %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Različica: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sürüm: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Версія: %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Phiên bản: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "版本:%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "版本:%@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "版本:%@\n" } } } }, "View" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ansicht" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ver" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Voir les détails…" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lihat" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Visualizza" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyświetl" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Visualizar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ver" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Просмотр" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pogled" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Göster" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Перегляд" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xem" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "查看" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "檢視" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "檢視" } } } }, "View announcement details" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ankündigungsdetails anzeigen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ver detalles del anuncio" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Voir les détails de l'annonce" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lihat detail pengumuman" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Visualizza i dettagli dell'annuncio" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "お知らせの詳細を表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "공지 사항 세부 정보 보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zobacz szczegóły ogłoszenia" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ver detalhes do anúncio" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ver detalhes do anúncio" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Просмотреть детали объявления" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť podrobnosti oznámenia" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ogled podrobnosti obvestila" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Duyuru detaylarını görüntüle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Переглянути деталі оголошення" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xem chi tiết thông báo" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "查看公告详情" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "查看公告詳情" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "查看公告詳情" } } } }, "View Changelog" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Änderungsprotokoll anzeigen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ver registro de cambios" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Voir le journal des modifications" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lihat Riwayat Perubahan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Visualizza il registro delle modifiche" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "変更履歴を表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "변경 로그 보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zobacz dziennik zmian" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ver registro de alterações" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ver Registo de Alterações" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Просмотреть журнал изменений" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť zmeny" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ogled sprememb" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Değişiklik günlüğünü görüntüle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Переглянути журнал змін" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xem nhật ký thay đổi" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "查看更改日志" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "查看變更日誌" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "查看更改日志" } } } }, "View Debug Window" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Zeige Debug Fenster" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ver ventana de depuración" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Afficher la fenêtre de débogage" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lihat Jendela Debug" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Visualizza finestra di debug" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "デバッグウィンドウを表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "디버그 창 보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyświetl okno debugowania" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ver Janela de Depuração" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ver Janela de Depuração" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Просмотр окна отладки" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť okno ladenia" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ogled okna za odpravljanje napak" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Hata Ayıklama Penceresini Görüntüle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Переглянути вікно налагодження" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xem cửa sổ gỡ lỗi" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "查看调试窗口" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "檢視除錯視窗" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "檢視除錯視窗" } } } }, "View homebrew-autoupdate.log" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "homebrew-autoupdate.log anzeigen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ver homebrew-autoupdate.log" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Voir homebrew-autoupdate.log" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lihat homebrew-autoupdate.log" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Visualizza homebrew-autoupdate.log" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "homebrew-autoupdate.logを表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "homebrew-autoupdate.log 보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zobacz homebrew-autoupdate.log" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ver homebrew-autoupdate.log" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ver homebrew-autoupdate.log" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Просмотр homebrew-autoupdate.log" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť homebrew-autoupdate.log" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ogled homebrew-autoupdate.log" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "homebrew-autoupdate.log dosyasını görüntüle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Переглянути homebrew-autoupdate.log" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xem homebrew-autoupdate.log" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "查看 homebrew-autoupdate.log" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "查看 homebrew-autoupdate.log" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "查看 homebrew-autoupdate.log" } } } }, "View in App Store" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Im App Store ansehen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ver en App Store" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Voir dans l'App Store" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lihat di App Store" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Visualizza su App Store" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "App Storeで表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "App Store에서 보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zobacz w App Store" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ver na App Store" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ver na App Store" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Посмотреть в App Store" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť v App Store" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ogled v trgovini App Store" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "App Store'da Görüntüle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Переглянути в App Store" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xem trong App Store" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "在 App Store 查看" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "在 App Store 查看" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "在 App Store 查看" } } } }, "View in Finder" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Im Finder zeigen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ver en Finder" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Afficher dans le Finder" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lihat di Finder" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Visualizza nel Finder" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Finder で表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Finder에서 보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyświetl w Finderze" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Visualizar no Finder" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ver no Finder" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Посмотреть в Finder" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť vo Finderi" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ogled v Finderju" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Finder’da Göster" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Переглянути у Finder" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xem trong Finder" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "在 Finder 中查看" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "在 Finder 檢視" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "在 Finder 檢視" } } } }, "View Issues" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Zeige Probleme" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ver problemas" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Voir les problèmes" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lihat Masalah" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Visualizza problemi" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "課題を表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "문제 보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyświetl problemy" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ver Problemas" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ver Problemas" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Просмотр проблем" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť Problémy" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ogled težav" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sorunları Görüntüle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Переглянути Проблеми" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xem các bấn đề" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "查看问题" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "檢視 Issue" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "檢視 Issue" } } } }, "View Log" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Protokoll anzeigen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ver registro" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Voir le journal" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lihat Log" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Visualizza registro" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ログを表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "로그 보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zobacz dziennik" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ver Log" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ver Registo" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Просмотреть журнал" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť denník" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ogled dnevnika" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Günlüğü Görüntüle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Переглянути журнал" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xem Nhật ký" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "查看日志" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "查看日誌" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "查看日誌" } } } }, "View packages in this tap" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Pakete in diesem Tab anzeigen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ver paquetes en esta pestaña" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Voir les paquets dans cet onglet" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lihat paket di ketukan ini" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Visualizza pacchetti in questo tap" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "このタップでパッケージを表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 탭에서 패키지 보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zobacz pakiety w tej zakładce" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ver pacotes nesta aba" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ver pacotes neste repositório" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Просмотр пакетов в этой вкладке" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť balíky v tejto karte" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ogled paketov v tem tapu" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu sekmede paketleri görüntüle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Переглянути пакунки в цій вкладці" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xem các gói trong tab này" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "在此标签中查看包" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "在此標籤中查看包" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "在此標籤中查看包" } } } }, "View project contributors" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Zeige Projekt-Mitarbeitende" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ver colaboradores del proyecto" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Voir les contributeurs du projet" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lihat kontributor proyek" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Visualizza i contributori del progetto" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "プロジェクトの貢献者を表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "프로젝트 기여자 보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyświetl osoby zaangażowane w projekt" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ver colaboradores do projeto" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ver contribuidores do projeto" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Посмотреть участников проекта" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť prispievateľov projektu" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ogled prispevkov k projektu" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Projeye katkıda bulunanları göster" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Переглянути вклад у проектування" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xem những người đóng góp cho dự án" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "查看项目贡献者" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "檢視項目貢獻者" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "檢視項目貢獻者" } } } }, "View Releases" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Zeige Version" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ver versiones" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Voir les versions" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lihat Rilis" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Visualizza versioni" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "リリースを表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "릴리즈 보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyświetl wydania" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ver Lançamentos" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ver Lançamentos" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Посмотреть выпуски" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť verzie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ogled izdaj" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Sürümleri Göster" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Переглянути Релізи" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xem các bản cập nhật" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "查看发布" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "檢視發行版本" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "檢視發行版本" } } } }, "View Repository" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Repository anzeigen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ver repositorio" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Voir le projet sur GitHub" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lihat Repositori" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Visualizza repository" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "リポジトリを表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "저장소 보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Wyświetl repozytorium " } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ver Repositório" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ver Repositório" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Посмотреть репозиторий" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť Repozitár" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ogled repozitorija" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Repository’i Göster" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Переглянути репозиторій" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Truy cập kho lưu trữ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "查看仓库" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "檢視原始碼 Repository" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "檢視原始碼 Repository" } } } }, "View TCC permissions for this app" : { }, "View Update" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Aktualisierung anzeigen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ver actualización" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Voir la mise à jour" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lihat Pembaruan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Visualizza aggiornamento" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "更新を表示" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "업데이트 보기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zobacz aktualizację" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ver Atualização" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ver Atualização" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Просмотр обновления" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť aktualizáciu" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ogled posodobitve" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Güncellemeyi Görüntüle" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Переглянути оновлення" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xem cập nhật" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "查看更新" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "查看更新" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "查看更新" } } } }, "View update details and download" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Aktualisierungsdetails anzeigen und herunterladen" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Ver detalles de la actualización y descargar" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Voir les détails de la mise à jour et télécharger" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Lihat detail pembaruan dan unduh" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Visualizza i dettagli dell'aggiornamento e scarica" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "更新の詳細を表示してダウンロード" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "업데이트 세부정보 보기 및 다운로드" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zobacz szczegóły aktualizacji i pobierz" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ver detalhes da atualização e baixar" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ver detalhes da atualização e descarregar" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Просмотреть детали обновления и скачать" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Zobraziť podrobnosti aktualizácie a stiahnuť" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ogled podrobnosti posodobitve in prenos" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Güncelleme ayrıntılarını görüntüle ve indir" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Переглянути деталі оновлення та завантажити" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Xem chi tiết cập nhật và tải xuống" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "查看更新详情并下载" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "查看更新詳情並下載" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "查看更新詳情並下載" } } } }, "Waiting for schedules to be configured" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Warten auf die Konfiguration der Zeitpläne" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Esperando que se configuren los horarios" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "En attente de la configuration des horaires" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Menunggu jadwal untuk dikonfigurasi" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "In attesa che i programmi siano configurati" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "スケジュールの設定を待っています" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "일정 구성을 기다리는 중" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Oczekiwanie na skonfigurowanie harmonogramów" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Aguardando a configuração dos horários" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A aguardar a configuração dos horários" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Ожидание настройки расписаний" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Čakanie na konfiguráciu plánov" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Čakanje na konfiguracijo urnikov" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Zaman çizelgelerinin yapılandırılmasını bekliyor" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Очікування налаштування розкладів" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Đang chờ lịch trình được cấu hình" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "等待日程配置" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "等待設定時間表" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "等待設定時間表" } } } }, "Warning" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Warnung" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Advertencia" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Avertissement" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Peringatan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Avviso" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "警告" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "경고" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ostrzeżenie" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Aviso" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Aviso" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Предупреждение" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Upozornenie" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Opozorilo" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uyarı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Попередження" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Cảnh báo" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "警告" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "警告" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "警告" } } } }, "Warning2" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Warnung!" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "¡Advertencia!" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Avertissement!" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Peringatan!" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Avviso2" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "警告!" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "경고2" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Ostrzeżenie2" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Aviso!" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Aviso2" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Предупреждение!" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Upozornenie!" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Opozorilo2" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uyarı!" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Попередження!" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Cảnh báo!" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "警告!" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "警告!" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "警告!" } } } }, "web" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "web" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "web" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "web" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "web" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "web" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ウェブ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "웹" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "sieć" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "web" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "web" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "веб" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "web" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "splet" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "web" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "веб" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "web" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "网络" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "網頁" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "網頁" } } } }, "Web app" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Webanwendung" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Aplicación web" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Application Web" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Aplikasi web" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "App web" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ウェブアプリ" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "웹 앱" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Aplikacja internetowa" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Aplicativo web" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Aplicação web" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Веб-приложение" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Webová aplikácia" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Spletna aplikacija" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Web uygulaması" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Веб програма" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Ứng dụng web" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "网页应用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "網頁 App" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "網頁 App" } } } }, "Weekly" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Wöchentlich" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Semanal" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Hebdomadaire" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Mingguan" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Settimanale" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "毎週" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "주간" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Tygodniowo" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Semanal" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Semanal" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Еженедельно" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Týždenne" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Tedensko" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Haftalık" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Щотижня" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Hàng tuần" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "每周" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "每星期" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "每星期" } } } }, "When applications are moved to Trash, Pearcleaner will launch and find related files and folders for deletion." : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Wenn Programme in den Papierkorb verschoben werden, startet Pearcleaner und sucht nach zugehörigen Dateien und Ordnern, um diese zu löschen." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cuando las aplicaciones se mueven a la Papelera, Pearcleaner se iniciará y encontrará archivos y carpetas relacionados para su eliminación." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Lorsque des applications sont déplacées dans la Corbeille, Pearcleaner se lance et trouve les fichiers et dossiers associés pour les supprimer." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ketika aplikasi dipindahkan ke Sampah, Pearcleaner akan diluncurkan dan menemukan file serta folder terkait untuk dihapus." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Quando le applicazioni vengono spostate nel Cestino, Pearcleaner si avvierà e troverà i file e le cartelle correlate per l'eliminazione." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "アプリケーションがゴミ箱に移動されると、Pearcleanerが起動し、関連するファイルとフォルダーを検索して削除します。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "애플리케이션이 휴지통으로 이동되면 Pearcleaner가 실행되어 삭제할 관련 파일 및 폴더를 찾습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Gdy aplikacje zostaną przeniesione do Kosza, Pearcleaner uruchomi się i wyszuka powiązane pliki i foldery do usunięcia." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Quando os aplicativos são movidos para o Lixo, o Pearcleaner será iniciado e encontrará arquivos e pastas relacionados para exclusão" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Quando aplicações são movidas para o Lixo, o Pearcleaner será iniciado e encontrará arquivos e pastas relacionados para exclusão." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Когда приложения будут перемещены в корзину, запустится программа Pearcleaner и найдет соответствующие файлы и папки для удаления." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Keď sa aplikácie presunú do koša, spustí sa Pearcleaner a nájde súvisiace súbory a priečinky na odstránenie." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ko so aplikacije premaknjene v koš, se bo Pearcleaner zagnal in poiskal povezane datoteke in mape za brisanje." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Uygulamalar Çöp Sepeti’ne taşınınca Pearcleaner çalışacak ve silmek için ilgili dosya ve klasörleri bulacaktır." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Коли програми буде переміщено до Смітника, Pearcleaner запуститься і знайде відповідні файли та папки для видалення." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Khi ứng dụng được đưa vào Thùng rác, Pearcleaner sẽ tự động khởi động và dò tìm các tệp, thư mục liên quan để xóa bỏ" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "当应用程序被移动到废纸篓时,Pearcleaner 将启动并查找相关的文件和文件夹以进行删除。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "當應用程式丟到垃圾桶時,Pearcleaner 會啟動並尋找要刪除的相關檔案和資料夾。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "當應用程式丟到垃圾桶時,Pearcleaner 會啟動並尋找要刪除的相關檔案和資料夾。" } } } }, "When deleting files using the Trash button, you can prevent accidental deletions by showing an alert before proceeding with the action." : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Zeige eine Warnung, damit vor dem versehentlich Löschen von Dateien im Papierkorb gewartet wird. " } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Al eliminar archivos con el botón de Papelera, puede evitar eliminaciones accidentales mostrando una alerta antes de continuar con la acción." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Lorsque vous supprimez des fichiers en utilisant le bouton Corbeille, vous pouvez éviter les suppressions accidentelles de fichiers/dossiers en affichant une alerte avant de procéder à l'action." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Saat menghapus file menggunakan tombol Sampah, Anda dapat mencegah penghapusan yang tidak disengaja dengan menampilkan peringatan sebelum melanjutkan tindakan tersebut." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Quando elimini file utilizzando il pulsante Cestino, puoi prevenire eliminazioni accidentali mostrando un avviso prima di procedere con l'azione." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ゴミ箱ボタンを使用してファイルを削除する場合、アラートを表示してからアクションを実行することで、誤った削除を防ぐことができます。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "휴지통 버튼을 사용하여 파일을 삭제할 때, 작업을 진행하기 전에 경고를 표시하여 실수로 삭제하는 것을 방지할 수 있습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Podczas usuwania plików za pomocą przycisku Kosz można zapobiec przypadkowemu usunięciu, wyświetlając ostrzeżenie przed wykonaniem tej czynności." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ao excluir arquivos usando o botão Lixeira, você pode evitar exclusões acidentais exibindo um alerta antes de prosseguir com a ação." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ao eliminar ficheiros usando o botão Lixo, pode evitar eliminações acidentais mostrando um alerta antes de prosseguir com a ação." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "При удалении файлов с помощью кнопки \"Удалить\" вы можете предотвратить случайное удаление, показав предупреждение перед выполнением действия." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pri odstraňovaní súborov pomocou tlačidla Koš môžeš zabrániť nechcenému odstráneni tým, že sa zobrazí upozornenie pred pokračovaním v akcii." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ko brišete datoteke z gumbom za smeti, lahko preprečite nenamerno brisanje tako, da pred nadaljevanjem prikažete opozorilo." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Çöp Sepeti butonunu kullanarak dosyalar silinirken, işleme devam etmeden önce ek bir uyarı göstererek kazara silmelerin önüne geçebilirsiniz." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Видаляючи файли за допомогою кнопки Смітник, ви можете запобігти випадковому видаленню, показавши попередження перед тим, як продовжити дію." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Khi xóa tệp bằng nút có biểu tượng Thùng rác, bạn có thể tránh xóa nhầm bằng cách hiển thị một thông báo xác nhận trước khi thực hiện hành động đó." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "使用废纸篓按钮删除文件时,可以通过在执行操作之前显示警报来防止意外删除。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "使用「垃圾桶」按鈕刪除檔案時,你可以在繼續此動作前顯示警告以避免意外刪除。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "使用「垃圾桶」按鈕刪除檔案時,你可以在繼續此動作前顯示警告以避免意外刪除。" } } } }, "When enabled, Pearcleaner will automatically scan for app updates in the background during launch. This way, updates are ready when you open the Updater view." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Wenn aktiviert, durchsucht Pearcleaner beim Start automatisch im Hintergrund nach App-Updates. Auf diese Weise sind die Updates bereit, wenn Sie die Aktualisierungsansicht öffnen." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cuando está habilitado, Pearcleaner buscará automáticamente actualizaciones de aplicaciones en segundo plano durante el inicio. De esta manera, las actualizaciones están listas cuando abres la vista de actualizaciones." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Lorsqu'elle est activée, Pearcleaner recherchera automatiquement les mises à jour des applications en arrière-plan lors du lancement. De cette façon, les mises à jour sont prêtes lorsque vous ouvrez la vue de mise à jour." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ketika diaktifkan, Pearcleaner akan secara otomatis memindai pembaruan aplikasi di latar belakang selama peluncuran. Dengan cara ini, pembaruan siap saat Anda membuka tampilan Pembaruan." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Quando abilitato, Pearcleaner eseguirà automaticamente la scansione degli aggiornamenti delle app in background durante l'avvio. In questo modo, gli aggiornamenti sono pronti quando apri la vista Aggiornamenti." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "有効にすると、Pearcleaner は起動時にバックグラウンドでアプリの更新を自動的にスキャンします。これにより、更新はアップデートビューを開いたときに準備が整います。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "활성화되면 Pearcleaner는 시작 시 백그라운드에서 앱 업데이트를 자동으로 검색합니다. 이렇게 하면 업데이트가 준비되어 업데이트 보기로 열 수 있습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Po włączeniu Pearcleaner automatycznie przeskanuje aktualizacje aplikacji w tle podczas uruchamiania. W ten sposób aktualizacje są gotowe, gdy otworzysz widok Aktualizatora." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Quando ativado, o Pearcleaner irá automaticamente procurar por atualizações de aplicativos em segundo plano durante o lançamento. Desta forma, as atualizações estão prontas quando você abrir a visualização do Atualizador." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Quando ativado, o Pearcleaner irá automaticamente procurar por atualizações de aplicativos em segundo plano durante o lançamento. Desta forma, as atualizações estão prontas quando você abrir a visualização do Atualizador." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Когда включено, Pearcleaner будет автоматически сканировать обновления приложений в фоновом режиме при запуске. Таким образом, обновления будут готовы, когда вы откроете окно обновления." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Keď je povolená, Pearcleaner automaticky skenuje aktualizácie aplikácií na pozadí počas spustenia. Týmto spôsobom sú aktualizácie pripravené, keď otvoríte zobrazenie aktualizácií." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ko je omogočeno, bo Pearcleaner samodejno iskal posodobitve aplikacij v ozadju med zagonom. Na ta način so posodobitve pripravljene, ko odprete pogled za posodobitev." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Etkinleştirildiğinde, Pearcleaner başlatma sırasında arka planda uygulama güncellemelerini otomatik olarak tarar. Bu şekilde, güncellemeler Güncelleyici görünümünü açtığınızda hazır olur." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Коли ввімкнено, Pearcleaner автоматично скануватиме оновлення додатків у фоновому режимі під час запуску. Таким чином, оновлення будуть готові, коли ви відкриєте вікно оновлення." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Khi được bật, Pearcleaner sẽ tự động quét các bản cập nhật ứng dụng trong nền khi khởi động. Bằng cách này, các bản cập nhật sẵn sàng khi bạn mở chế độ xem Cập nhật." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "启用后,Pearcleaner将在启动时自动在后台扫描应用更新。这样,当您打开更新视图时,更新就已准备就绪。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "啟用後,Pearcleaner 將在啟動時自動在背景中掃描應用程式更新。這樣,當您打開更新視圖時,更新就準備好了。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "啟用後,Pearcleaner 將在啟動時自動在背景中掃描應用程式更新。這樣,當您打開更新視圖時,更新就準備好了。" } } } }, "When executing certain write actions, show an alert before proceeding." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Beim Ausführen bestimmter Schreibaktionen, zeige eine Warnung, bevor du fortfährst." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Al ejecutar ciertas acciones de escritura, muestra una alerta antes de continuar." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Lors de l'exécution de certaines actions d'écriture, affichez une alerte avant de continuer." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Saat menjalankan tindakan penulisan tertentu, tampilkan peringatan sebelum melanjutkan." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Quando si eseguono determinate azioni di scrittura, mostra un avviso prima di procedere." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "特定の書き込みアクションを実行する際には、続行する前に警告を表示します。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "특정 쓰기 작업을 실행할 때 진행하기 전에 경고를 표시합니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Podczas wykonywania niektórych działań zapisu, pokaż alert przed kontynuacją." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ao executar certas ações de escrita, mostre um alerta antes de prosseguir." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ao executar certas ações de escrita, mostrar um alerta antes de prosseguir." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "При выполнении некоторых действий записи, показывайте предупреждение перед продолжением." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pri vykonávaní určitých akcií zápisu zobrazte upozornenie pred pokračovaním." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Pri izvajanju določenih akcij pisanja prikaži opozorilo, preden nadaljuješ." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Belirli yazma işlemleri gerçekleştirildiğinde, devam etmeden önce bir uyarı göster." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "При виконанні певних дій запису, показуйте попередження перед продовженням." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Khi thực hiện các hành động ghi nhất định, hãy hiển thị cảnh báo trước khi tiếp tục." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "执行某些写入操作时,请先显示警告。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "執行某些寫入操作時,請先顯示警告。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "執行某些寫入操作時,請先顯示警告。" } } } }, "When Homebrew cleanup is enabled, Pearcleaner will check if the app you are removing was installed via Homebrew and launch Terminal.app to execute a brew uninstall and cleanup command to let Homebrew know that the app is removed. This way your Homebrew list will be synced up correctly and caching will be removed. Terminal.app is required since some apps need sudo permissions to remove services and files placed in system folders. Since other terminal apps don't support AppleScript and/or the 'do script' command, I opted to use the default macOS Terminal app for this.\n\nNOTE: If you undo the file delete with CMD+Z, the files will be put back but Homebrew will not be aware of it. To get the Homebrew list back in sync you'd need to run:\n\n> brew install APPNAME --force" : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Wenn die Homebrew-Bereinigung aktiviert ist, prüft Pearcleaner, ob die zu entfernende Anwendung über Homebrew installiert wurde und startet Terminal.app, um einen Befehl zum Deinstallieren und Bereinigen von Homebrew auszuführen, damit Homebrew weiss, dass die Anwendung entfernt wurde. Auf diese Weise wird Ihre Homebrew-Liste korrekt synchronisiert und die Zwischenspeicherung wird entfernt. Terminal.app ist erforderlich, da einige Anwendungen sudo-Berechtigungen benötigen, um Dienste und Dateien in Systemordnern zu entfernen. Da andere Terminal-Apps AppleScript und/oder den Befehl \"do script\" nicht unterstützen, habe ich mich dafür entschieden, die Standard macOS Terminal-App zu verwenden.\n\nHINWEIS: Wenn Sie das Löschen von Dateien mit CMD+Z rückgängig machen, werden die Dateien wiederhergestellt, aber Homebrew wird davon nichts mitbekommen. Um die Homebrew-Liste wieder zu synchronisieren, müssen Sie den Befehl ausführen:\n\n> brew install APPNAME --force" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cuando la limpieza de Homebrew está habilitada, Pearcleaner verificará si la aplicación que estás eliminando se instaló a través de Homebrew y abrirá Terminal.app para ejecutar un comando de desinstalación y limpieza de brew para informar a Homebrew que la aplicación ha sido eliminada. De esta forma, tu lista de Homebrew se sincronizará correctamente y se eliminará el almacenamiento en caché. Se requiere Terminal.app ya que algunas aplicaciones necesitan permisos de sudo para eliminar servicios y archivos ubicados en carpetas del sistema. Dado que otras aplicaciones de terminal no soportan AppleScript y/o el comando 'do script', opté por usar la aplicación Terminal predeterminada de macOS para esto\n\nNOTA: Si deshaces la eliminación del archivo con CMD+Z, los archivos se restaurarán pero Homebrew no estará al tanto de ello. Para volver a sincronizar la lista de Homebrew, necesitarías ejecutar:\n\n> brew install APPNAME --force" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Lorsque le nettoyage Homebrew est activé, Pearcleaner vérifiera si l'application que vous supprimez a été installée via Homebrew et lancera Terminal.app pour exécuter une commande de désinstallation et de nettoyage brew afin de signaler à Homebrew que l'application a été supprimée. De cette façon, votre liste Homebrew sera correctement synchronisée et le cache sera supprimé. Terminal.app est requis car certaines applications nécessitent des autorisations sudo pour supprimer des services et des fichiers placés dans des dossiers système. Étant donné que d'autres applications de terminal ne prennent pas en charge AppleScript et/ou la commande 'do script', Pearcleaner a choisi d'utiliser l'application Terminal par défaut de macOS pour cela.\n\nNOTE : Si vous annulez la suppression du fichier avec ⌘+Z, les fichiers seront remis en place mais Homebrew ne sera pas au courant. Pour resynchroniser la liste Homebrew, vous devrez exécuter :\n \n> brew install APPNAME --force" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Saat Homebrew cleanup diaktifkan, Pearcleaner akan memeriksa apakah aplikasi yang Anda hapus diinstal melalui Homebrew dan meluncurkan Terminal.app untuk menjalankan perintah brew uninstall dan cleanup agar Homebrew tahu bahwa aplikasi tersebut telah dihapus. Dengan cara ini, daftar Homebrew Anda akan disinkronkan dengan benar dan caching akan dihapus. Terminal.app diperlukan karena beberapa aplikasi memerlukan izin sudo untuk menghapus layanan dan file yang ditempatkan di folder sistem. Karena aplikasi terminal lain tidak mendukung AppleScript dan/atau perintah 'do script', saya memilih untuk menggunakan aplikasi Terminal bawaan macOS untuk ini.\n\nCATATAN: Jika Anda membatalkan penghapusan file dengan CMD+Z, file akan dikembalikan tetapi Homebrew tidak akan menyadarinya. Untuk menyinkronkan kembali daftar Homebrew, Anda perlu menjalankan:\n\n> brew install APPNAME --force" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Quando la pulizia di Homebrew è abilitata, Pearcleaner controllerà se l'app che stai rimuovendo è stata installata tramite Homebrew e avvierà Terminal.app per eseguire un comando di disinstallazione e pulizia di brew per informare Homebrew che l'app è stata rimossa. In questo modo, la tua lista di Homebrew sarà sincronizzata correttamente e la cache verrà rimossa. Terminal.app è necessario poiché alcune app richiedono permessi sudo per rimuovere servizi e file posizionati nelle cartelle di sistema. Poiché altre app terminali non supportano AppleScript e/o il comando 'do script', ho optato per utilizzare l'app Terminal predefinita di macOS per questo.\n\nNOTA: Se annulli l'eliminazione del file con CMD+Z, i file verranno ripristinati ma Homebrew non ne sarà a conoscenza. Per riportare la lista di Homebrew in sincronia, dovresti eseguire:\n\n> brew install APPNAME --force" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Homebrewのクリーンアップが有効になっている場合、Pearcleanerはアプリを削除する際にHomebrewを介してインストールされたかどうかを確認し、Terminal.appを起動してbrew uninstallとクリーンアップコマンドを実行して、Homebrewにアプリが削除されたことを通知します。この方法でHomebrewのリストが正しく同期され、キャッシュが削除されます。システムフォルダーに配置されたサービスやファイルを削除するには、一部のアプリにsudo権限が必要なため、Terminal.appが必要です。他のターミナルアプリはAppleScriptや'do script'コマンドをサポートしていないため、デフォルトのmacOS Terminalアプリを使用することにしました。\n\n注意: CMD+Zでファイル削除を元に戻すと、ファイルは元に戻りますが、Homebrewはそれを認識しません。Homebrewのリストを再度同期させるには、次のコマンドを実行する必要があります:\n\n> brew install APPNAME --force" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 정리가 활성화되면 Pearcleaner는 제거하려는 앱이 Homebrew를 통해 설치되었는지 확인하고 Terminal.app을 실행하여 brew uninstall 및 cleanup 명령을 실행하여 Homebrew에서 앱이 되었음을 알립니다. 이렇게 하면 Homebrew 목록이 올바르게 동기화되고 캐싱이 제거됩니다. 일부 앱은 시스템 폴더에 배치된 서비스 및 파일을 제거하기 위해 sudo 권한이 필요하므로 Terminal.app이 필요합니다. 다른 터미널 앱은 AppleScript 및/또는 'do script' 명령을 지원하지 않으므로 기본 macOS Terminal 앱을 사용하게 되었습니다.\n\n참고: CMD+Z로 파일 삭제를 실행 취소하면 파일이 복원되지만 Homebrew는 이를 인식하지 못합니다. Homebrew 목록을 다시 동기화하려면 다음 명령어를 실행해야 합니다:\n\n> brew install APPNAME --force" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Gdy włączona jest funkcja czyszczenia Homebrew, Pearcleaner sprawdzi, czy usuwana aplikacja została zainstalowana za pomocą Homebrew, i uruchomi aplikację Terminal.app, aby wykonać polecenie brew uninstall i cleanup, informujące Homebrew o usunięciu aplikacji. W ten sposób lista Homebrew zostanie poprawnie zsynchronizowana, a pamięć podręczna zostanie usunięta. Aplikacja Terminal.app jest wymagana, ponieważ niektóre aplikacje potrzebują uprawnień sudo do usuwania usług i plików umieszczonych w folderach systemowych. Ponieważ inne aplikacje terminalowe nie obsługują AppleScript i/lub polecenia ‘do script’, zdecydowałem się użyć do tego domyślnej aplikacji Terminal w systemie macOS.\n\nUWAGA: Jeśli cofniesz usunięcie plików za pomocą CMD+Z, pliki zostaną przywrócone, ale Homebrew nie będzie o tym wiedzieć. Aby ponownie zsynchronizować listę Homebrew, należy uruchomić:\n\n> brew install NAZWA-APLIKACJI --force" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Quando a limpeza do Homebrew está ativada, o Pearcleaner verificará se o aplicativo que você está removendo foi instalado via Homebrew e lançará o Terminal.app para executar um comando de desinstalação e limpeza do brew para informar ao Homebrew que o aplicativo foi removido. Dessa forma, sua lista do Homebrew será sincronizada corretamente e o cache será removido. O Terminal.app é necessário, pois alguns aplicativos precisam de permissões sudo para remover serviços e arquivos colocados em pastas do sistema. Como outros aplicativos de terminal não suportam AppleScript e/ou o comando 'do script', optei por usar o aplicativo padrão Terminal do macOS para isso.\n\nNOTA: Se você desfizer a exclusão de arquivos com CMD+Z, os arquivos serão reinstaurados, mas o Homebrew não ficará ciente disso. Para colocar a lista do Homebrew em sincronia novamente, você precisaria executar:\n\n> brew install APPNAME --force" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Quando a limpeza do Homebrew está ativada, o Pearcleaner verificará se o aplicativo que você está removendo foi instalado via Homebrew e lançará o Terminal.app para executar um comando de desinstalação e limpeza do brew para informar ao Homebrew que o aplicativo foi removido. Desta forma, a sua lista do Homebrew será sincronizada corretamente e o cache será removido. O Terminal.app é necessário, pois alguns aplicativos precisam de permissões sudo para remover serviços e arquivos colocados em pastas do sistema. Como outros aplicativos de terminal não suportam AppleScript e/ou o comando 'do script', optei por usar o aplicativo Terminal padrão do macOS para isso.\n\nNOTA: Se você desfizer a exclusão do arquivo com CMD+Z, os arquivos serão restaurados, mas o Homebrew não estará ciente disso. Para sincronizar novamente a lista do Homebrew, você precisará executar:\n\n> brew install APPNAME --force" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Когда включена очистка Homebrew, Pearcleaner проверит, был ли удаляемый вами приложение установлено через Homebrew, и запустит Terminal.app для выполнения команды brew uninstall и последующей очистки — чтобы Homebrew узнал, что приложение удалено. Благодаря этому список Homebrew останется корректным, а кэш будет очищен.\n\nДля этой операции требуется Terminal.app, поскольку некоторые приложения требуют прав sudo для удаления служб и файлов, размещённых в системных папках. Другие терминальные приложения не поддерживают AppleScript и/или команду do script, поэтому я выбрал стандартный терминал macOS.\n\nПРИМЕЧАНИЕ: Если вы отмените удаление файлов с помощью Cmd+Z, файлы вернутся, но Homebrew об этом не узнает. Чтобы синхронизировать список Homebrew заново, выполните: \n\n> brew install APPNAME --force" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Po zapnutí vyčistenia Homebrew, Pearcleaner skontroluje, či bola aplikácia, ktorú odstraňuješ nainštalovaná pomocou Homebrew. Následne spustí Terminal.app a vykoná príkaz brew uninstall a brew cleanup a informuje Homebrew o odstránení aplikácie. Takto sa Homebrew zoznam správne synchronizuje a vyrovnácia pamäť vyčistí. Terminal.app je potrebná, pretože niektoré aplikácie vyžadujú sudo oprávnenia na odstránenie služieb a súborov umiestnených v systémových priečinkoch. Keďže iné terminálové aplikácie nepodporujú AppleScript a/alebo do script príkaz, rozhodol som sa použiť predvolenú aplikáciu Terminal.\n\nUPOZORNENIE: Ak vrátiš späť odstránenie súborov pomocou CMD+Z, súbory sa obnovia, ale Homebrew o tom nebude vedieť. Na opätovné zosynchronizovanie zoznamu Homebrew budeš musieť spustiť príkaz:\n\n> brew install NAZOVAPLIKACIE --force" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ko je omogočeno čiščenje Homebrew, bo Pearcleaner preveril, ali je bila aplikacija, ki jo odstranjujete, nameščena prek Homebrew, in zagnal Terminal.app za izvedbo ukaza za odstranitev in čiščenje, da Homebrew obvesti, da je aplikacija odstranjena. Tako bo vaš seznam Homebrew pravilno sinhroniziran in predpomnjenje bo odstranjeno. Terminal.app je potreben, ker nekatere aplikacije potrebujejo sudo dovoljenja za odstranitev storitev in datotek, nameščenih v sistemske mape. Ker druge terminalske aplikacije ne podpirajo AppleScript in/ali ukaza 'do script', sem se odločil uporabiti privzeto aplikacijo Terminal macOS za to.\n\nOPOMBA: Če razveljavite brisanje datoteke s CMD+Z, bodo datoteke vrnjene, vendar Homebrew tega ne bo vedel. Da bi seznam Homebrew ponovno sinhronizirali, bi morali zagnati:\n\n> brew install APPNAME --force" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew temizliği etkinken, Pearcleaner kaldırdığınız uygulamanın Homebrew aracılığı ile yüklenip yüklenmediğini kontrol edecek ve Homebrew’e uygulamanın kaldırıldığını bildirmek amacıyla brew uninstall ve cleanup komutlarını çalıştırmak için Terminal.app’i açacaktır. Böylece Homebrew listeniz düzgünce senkronize olacak ve önbellekleme kaldırılacaktır. Terminal.app, bazı uygulamalar sistem klasörlerindeki dosyalar ve servisleri kaldırmak için sudo izinlerine ihtiyaç duyduğu için gereklidir. Diğer terminal uygulamaları AppleScript ve/veya ‘do script’ komutunu desteklemediğinden bunun için varsayılan macOS Terminal uygulamasını kullanmayı seçtim.\n\nNOT: CMD+Z ile dosya silme işlemini geri alırsanız, dosyalar geri konacaktır ancak Homebrew bunun farkında olmayacaktır. Homebrew listesini tekrar senkronize etmek için şu komutu çalıştırmanız gerekir:\n\n> brew install UYGULAMAADI --force" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Якщо очищення Homebrew увімкнено, Pearcleaner перевірить, чи програму, яку ви видаляєте, було встановлено через Homebrew, і запустить програму Terminal.app для виконання команди видалення і очищення, щоб повідомити Homebrew, що програму видалено. Таким чином ваш список Homebrew буде правильно синхронізовано, а кешування буде видалено. Terminal.app є необхідним, оскільки деякі програми потребують дозволів sudo для видалення служб і файлів, розміщених у системних теках. Оскільки інші термінальні програми не підтримують AppleScript та/або команду «do script», я вирішив використати для цього стандартну програму macOS Terminal.\n\nПРИМІТКА: Якщо ви скасуєте видалення файлів за допомогою CMD+Z, файли буде відновлено, але Homebrew не знатиме про це. Щоб відновити синхронізацію списку Homebrew, вам потрібно виконати\n\n> brew install APPNAME --force" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Khi bật tính năng dọn \"Homebrew cleanup\", Pearcleaner sẽ kiểm tra xem ứng dụng đang gỡ có được cài đặt thông qua Homebrew hay không và sẽ mở ứng dụng Terminal.app để chạy lệnh brew uninstall và cleanup, nhằm thông báo cho Homebrew biết rằng ứng dụng đã bị gỡ.\nCách này giúp danh sách ứng dụng trong Homebrew được đồng bộ chính xác và loại bỏ các tệp bộ nhớ đệm.\nỨng dụng Terminal.app mặc định của macOS là bắt buộc vì một số ứng dụng cần quyền sudo để gỡ bỏ dịch vụ và các tệp nằm trong thư mục hệ thống. Do các ứng dụng terminal khác không hỗ trợ AppleScript và/hoặc lệnh 'do script', do đó Pearcleaner đã chọn sử dụng ứng dụng Terminal.app mặc định của macOS cho thao tác này.\n\nLưu ý: Nếu bạn hoàn tác việc xóa tệp bằng phím tắt CMD+Z, các tệp sẽ được khôi phục, nhưng Homebrew sẽ không biết điều đó.\nĐể đồng bộ lại danh sách Homebrew, bạn cần chạy lệnh:\n\n> brew install APPNAME --force" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "当启用 Homebrew 清理时,Pearcleaner 将检查您要删除的应用程序是否通过 Homebrew 安装,并启动 终端.app 来执行brew uninstall和cleanup命令,以便让Homebrew知道该应用已被删除。这样,您的Homebrew列表将正确同步,缓存将被移除。需要 终端.app,因为某些应用程序需要 sudo 权限才能删除服务和放置在系统文件夹中的文件。由于其他终端应用程序不支持 AppleScript 和/或 'do script' 命令,因此我选择使用默认的 macOS 终端 应用程序。\n\n注意:如果您使用CMD+Z撤销文件删除,文件将被恢复,但Homebrew将不会意识到这一点。要使Homebrew列表重新同步,您需要运行:\n\n> brew install APPNAME --force" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "啟用「Homebrew 清理」時,Pearcleaner 會檢查你要移除的應用程式是否透過 Homebrew 安裝,並啟動「終端機.app」以執行 brew 解除安裝和清理命令,讓 Homebrew 知道此應用程式已移除。這樣你的 Homebrew 列表會正確同步,快取檔案亦會被移除。這需要「終端機.app」,因為一些應用程式需要 sudo 權限才能移除系統資料夾中的服務和檔案。由於其他終端機應用程式不支援 AppleScript 和/或「do script」命令,因此這裡使用預設的 macOS 終端機應用程式。\n\n注意:如你使用 ⌘Z 還原刪除檔案,檔案將會放回,但 Homebrew 並不會注意到這一點。如要重新同步 Homebrew 列表,你需要執行:\n\n> brew install APPNAME --force" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "啟用「Homebrew 清理」時,Pearcleaner 會檢查你要移除的應用程式是否透過 Homebrew 安裝,並啟動「終端機.app」以執行 brew 解除安裝和清理命令,讓 Homebrew 知道此應用程式已移除。這樣你的 Homebrew 列表會正確同步,快取檔案亦會被移除。這需要「終端機.app」,因為一些應用程式需要 sudo 權限才能移除系統資料夾中的服務和檔案。由於其他終端機應用程式不支援 AppleScript 和/或「do script」命令,因此這裡使用預設的 macOS 終端機應用程式。\n\n注意:如你使用 ⌘Z 還原刪除檔案,檔案將會放回,但 Homebrew 並不會注意到這一點。如要重新同步 Homebrew 列表,你需要執行:\n\n> brew install APPNAME --force" } } } }, "When Homebrew cleanup is enabled, Pearcleaner will check if the app you are removing was installed via Homebrew and remove the cache to keep everything synced up." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Wenn die Homebrew-Bereinigung aktiviert ist, überprüft Pearcleaner, ob die App, die Sie entfernen, über Homebrew installiert wurde, und entfernt den Cache, um alles synchron zu halten." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cuando la limpieza de Homebrew está habilitada, Pearcleaner verificará si la aplicación que está eliminando fue instalada a través de Homebrew y eliminará el caché para mantener todo sincronizado." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Lorsque le nettoyage Homebrew est activé, Pearcleaner vérifiera si l'application que vous supprimez a été installée via Homebrew et supprimera le cache pour tout synchroniser." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ketika pembersihan Homebrew diaktifkan, Pearcleaner akan memeriksa apakah aplikasi yang Anda hapus diinstal melalui Homebrew dan menghapus cache untuk menjaga semuanya tetap sinkron." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Quando la pulizia di Homebrew è abilitata, Pearcleaner verificherà se l'app che stai rimuovendo è stata installata tramite Homebrew e rimuoverà la cache per mantenere tutto sincronizzato." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew のクリーンアップが有効になっている場合、Pearcleaner は削除しているアプリが Homebrew 経由でインストールされたかどうかを確認し、すべてを同期させるためにキャッシュを削除します。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 정리가 활성화되면 Pearcleaner는 제거 중인 앱이 Homebrew를 통해 설치되었는지 확인하고 캐시를 제거하여 모든 것을 동기화 상태로 유지합니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Gdy czyszczenie Homebrew jest włączone, Pearcleaner sprawdzi, czy aplikacja, którą usuwasz, została zainstalowana za pomocą Homebrew i usunie pamięć podręczną, aby wszystko było zsynchronizowane." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Quando a limpeza do Homebrew está ativada, o Pearcleaner verificará se o aplicativo que você está removendo foi instalado via Homebrew e removerá o cache para manter tudo sincronizado." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Quando a limpeza do Homebrew está ativada, o Pearcleaner verificará se a aplicação que está a remover foi instalada via Homebrew e removerá o cache para manter tudo sincronizado." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Когда очистка Homebrew включена, Pearcleaner проверит, была ли удаляемая вами программа установлена через Homebrew, и удалит кэш, чтобы все оставалось синхронизированным." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Keď je povolené čistenie Homebrew, Pearcleaner skontroluje, či aplikácia, ktorú odstraňujete, bola nainštalovaná cez Homebrew, a odstráni vyrovnávaciu pamäť, aby všetko zostalo synchronizované." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ko je omogočeno čiščenje Homebrew, bo Pearcleaner preveril, ali je aplikacija, ki jo odstranjujete, nameščena prek Homebrew, in odstranil predpomnilnik, da bo vse sinhronizirano." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew temizliği etkinleştirildiğinde, Pearcleaner kaldırmakta olduğunuz uygulamanın Homebrew üzerinden yüklenip yüklenmediğini kontrol edecek ve her şeyin senkronize kalması için önbelleği kaldıracaktır." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Коли увімкнено очищення Homebrew, Pearcleaner перевірить, чи була програма, яку ви видаляєте, встановлена через Homebrew, і видалить кеш, щоб усе залишалося синхронізованим." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Khi dọn dẹp Homebrew được bật, Pearcleaner sẽ kiểm tra xem ứng dụng bạn đang gỡ bỏ có được cài đặt qua Homebrew hay không và xóa bộ nhớ đệm để giữ mọi thứ đồng bộ." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "当启用 Homebrew 清理时,Pearcleaner 会检查您正在移除的应用程序是否通过 Homebrew 安装,并移除缓存以保持所有内容同步。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "當啟用 Homebrew 清理時,Pearcleaner 會檢查您正在移除的應用程式是否通過 Homebrew 安裝,並移除緩存以保持所有內容同步。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "當啟用 Homebrew 清理時,Pearcleaner 會檢查您正在移除的應用程式是否通過 Homebrew 安裝,並移除緩存以保持所有內容同步。" } } } }, "When running privileged Homebrew operations, Pearcleaner caches your password in the macOS Keychain for this duration to avoid repeated password prompts. Homebrew commands cannot be executed with the privileged helper tool Pearcleaner offers." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Bei der Ausführung privilegierter Homebrew-Operationen speichert Pearcleaner Ihr Passwort für diesen Zeitraum im macOS-Schlüsselbund, um wiederholte Passwortabfragen zu vermeiden. Homebrew-Befehle können nicht mit dem von Pearcleaner angebotenen privilegierten Hilfsprogramm ausgeführt werden." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Al ejecutar operaciones de Homebrew con privilegios, Pearcleaner guarda su contraseña en el llavero de macOS durante este tiempo para evitar solicitudes repetidas de contraseña. Los comandos de Homebrew no se pueden ejecutar con la herramienta auxiliar privilegiada que ofrece Pearcleaner." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Lors de l'exécution d'opérations Homebrew avec privilèges, Pearcleaner met en cache votre mot de passe dans le trousseau macOS pour cette durée afin d'éviter les demandes répétées de mot de passe. Les commandes Homebrew ne peuvent pas être exécutées avec l'outil d'assistance privilégié proposé par Pearcleaner." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Saat menjalankan operasi Homebrew dengan hak istimewa, Pearcleaner menyimpan kata sandi Anda di macOS Keychain untuk durasi ini guna menghindari permintaan kata sandi berulang. Perintah Homebrew tidak dapat dijalankan dengan alat bantu istimewa yang ditawarkan Pearcleaner." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Quando si eseguono operazioni Homebrew privilegiate, Pearcleaner memorizza la tua password nel Portachiavi di macOS per questa durata per evitare ripetute richieste di password. I comandi Homebrew non possono essere eseguiti con lo strumento di supporto privilegiato offerto da Pearcleaner." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "特権を持つHomebrew操作を実行する際、Pearcleanerはこの期間中、macOSキーチェーンにパスワードをキャッシュして、繰り返しパスワードを求められるのを避けます。Homebrewコマンドは、Pearcleanerが提供する特権ヘルパーツールでは実行できません。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "권한이 있는 Homebrew 작업을 실행할 때 Pearcleaner는 반복적인 비밀번호 입력을 피하기 위해 이 기간 동안 macOS 키체인에 비밀번호를 캐시합니다. Homebrew 명령은 Pearcleaner가 제공하는 권한 있는 도우미 도구로 실행할 수 없습니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Podczas uruchamiania uprzywilejowanych operacji Homebrew, Pearcleaner przechowuje twoje hasło w pęku kluczy macOS przez ten czas, aby uniknąć powtarzających się monitów o hasło. Polecenia Homebrew nie mogą być wykonywane za pomocą uprzywilejowanego narzędzia pomocniczego oferowanego przez Pearcleaner." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Ao executar operações privilegiadas do Homebrew, o Pearcleaner armazena sua senha no Chaveiro do macOS por este período para evitar solicitações repetidas de senha. Os comandos do Homebrew não podem ser executados com a ferramenta auxiliar privilegiada oferecida pelo Pearcleaner." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Ao executar operações privilegiadas do Homebrew, o Pearcleaner armazena sua senha no Chaveiro do macOS por este período para evitar solicitações de senha repetidas. Os comandos do Homebrew não podem ser executados com a ferramenta auxiliar privilegiada que o Pearcleaner oferece." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "При выполнении привилегированных операций Homebrew, Pearcleaner кэширует ваш пароль в связке ключей macOS на этот период, чтобы избежать повторных запросов пароля. Команды Homebrew не могут быть выполнены с помощью привилегированного инструмента, предлагаемого Pearcleaner." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pri spustení privilegovaných operácií Homebrew, Pearcleaner ukladá vaše heslo do kľúčenky macOS na toto obdobie, aby sa predišlo opakovaným výzvam na zadanie hesla. Príkazy Homebrew nemožno vykonať s privilegovaným pomocným nástrojom, ktorý ponúka Pearcleaner." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ko izvajate privilegirane operacije Homebrew, Pearcleaner za to obdobje shrani vaše geslo v macOS Keychain, da se izognete ponavljajočim se pozivom za geslo. Homebrew ukazov ni mogoče izvesti s privilegiranim orodjem, ki ga ponuja Pearcleaner." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Ayrıcalıklı Homebrew işlemleri çalıştırılırken, Pearcleaner şifrenizi bu süre boyunca macOS Anahtar Zinciri'nde önbelleğe alır, böylece tekrar eden şifre istemlerinden kaçınılır. Homebrew komutları, Pearcleaner'ın sunduğu ayrıcalıklı yardımcı araçla çalıştırılamaz." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Під час виконання привілейованих операцій Homebrew, Pearcleaner кешує ваш пароль у macOS Keychain на цей період, щоб уникнути повторних запитів пароля. Команди Homebrew не можуть бути виконані за допомогою привілейованого інструменту, який пропонує Pearcleaner." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Khi chạy các thao tác Homebrew có quyền ưu tiên, Pearcleaner lưu mật khẩu của bạn trong macOS Keychain trong thời gian này để tránh các yêu cầu nhập mật khẩu lặp lại. Các lệnh Homebrew không thể được thực thi với công cụ trợ giúp có quyền ưu tiên mà Pearcleaner cung cấp." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "在运行需要权限的 Homebrew 操作时,Pearcleaner 会在此期间将您的密码缓存到 macOS 钥匙串中,以避免重复的密码提示。Homebrew 命令无法使用 Pearcleaner 提供的特权辅助工具执行。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "當運行需要特權的 Homebrew 操作時,Pearcleaner 會在此期間將您的密碼緩存到 macOS 鑰匙串中,以避免重複的密碼提示。Homebrew 命令無法使用 Pearcleaner 提供的特權輔助工具執行。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "當運行需要特權的 Homebrew 操作時,Pearcleaner 會在此期間將您的密碼緩存到 macOS 鑰匙串中,以避免重複的密碼提示。Homebrew 命令無法使用 Pearcleaner 提供的特權輔助工具執行。" } } } }, "When the helper tool was introduced March 2025, it was said that AuthorizationExecuteWithPrivileges (granting authentication via password prompt popup) would eventually be removed as it has already been deprecated by Apple. Some functionality will stop working in Pearcleaner if the helper isn't enabled going forward." : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Als das Hilfswerkzeug im März 2025 eingeführt wurde, hieß es, dass AuthorizationExecuteWithPrivileges (Gewährung der Authentifizierung über ein Passwort-Popup) schließlich entfernt werden würde, da es bereits von Apple veraltet ist. Einige Funktionen werden in Pearcleaner nicht mehr funktionieren, wenn das Hilfswerkzeug nicht aktiviert ist." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cuando se introdujo la herramienta auxiliar en marzo de 2025, se dijo que AuthorizationExecuteWithPrivileges (que otorga autenticación mediante un cuadro de diálogo de solicitud de contraseña) eventualmente sería eliminado ya que Apple ya lo había desaprobado. Algunas funciones dejarán de funcionar en Pearcleaner si el asistente no está habilitado en el futuro." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Lorsque l'outil d'assistance a été introduit en mars 2025, il a été dit que AuthorizationExecuteWithPrivileges (accordant l'authentification via une fenêtre contextuelle de demande de mot de passe) serait éventuellement supprimé car il a déjà été déprécié par Apple. Certaines fonctionnalités cesseront de fonctionner dans Pearcleaner si l'outil d'assistance n'est pas activé." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ketika alat bantu diperkenalkan pada Maret 2025, dikatakan bahwa AuthorizationExecuteWithPrivileges (memberikan otentikasi melalui jendela pop-up permintaan kata sandi) pada akhirnya akan dihapus karena sudah tidak digunakan lagi oleh Apple. Beberapa fungsi akan berhenti bekerja di Pearcleaner jika alat bantu tidak diaktifkan." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Quando lo strumento di supporto è stato introdotto a marzo 2025, è stato detto che AuthorizationExecuteWithPrivileges (che concede l'autenticazione tramite popup di richiesta password) sarebbe stato eventualmente rimosso poiché è già stato deprecato da Apple. Alcune funzionalità smetteranno di funzionare in Pearcleaner se lo strumento di supporto non è abilitato." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "2025年3月にヘルパーツールが導入された際、AuthorizationExecuteWithPrivileges(パスワードプロンプトポップアップを介した認証の付与)は最終的に削除されると言われました。これはすでにAppleによって廃止されているためです。ヘルパーが有効になっていない場合、Pearcleanerの一部の機能は動作しなくなります。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "도우미 도구가 2025년 3월에 도입되었을 때, Apple에 의해 사용 중단된 AuthorizationExecuteWithPrivileges(비밀번호 프롬프트 팝업을 통한 인증 부여)가 제거될 것이라 소개했습니다. 도우미가 활성화되지 않으면 Pearcleaner의 일부 기능이 작동하지 않을 것입니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Kiedy narzędzie pomocnicze zostało wprowadzone w marcu 2025 roku, powiedziano, że AuthorizationExecuteWithPrivileges (udzielanie uwierzytelnienia za pomocą wyskakującego okienka z hasłem) zostanie ostatecznie usunięte, ponieważ zostało już wycofane przez Apple. Niektóre funkcje przestaną działać w Pearcleaner, jeśli narzędzie pomocnicze nie zostanie włączone." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Quando a ferramenta auxiliar foi introduzida em março de 2025, foi dito que AuthorizationExecuteWithPrivileges (concedendo autenticação através de uma janela pop-up de solicitação de senha) seria eventualmente removido, pois já foi descontinuado pela Apple. Algumas funcionalidades deixarão de funcionar no Pearcleaner se a ferramenta auxiliar não estiver ativada." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Quando a ferramenta auxiliar foi introduzida em março de 2025, foi dito que o AuthorizationExecuteWithPrivileges (concedendo autenticação via popup de solicitação de senha) seria eventualmente removido, pois já foi descontinuado pela Apple. Algumas funcionalidades deixarão de funcionar no Pearcleaner se o auxiliar não estiver ativado daqui para frente." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Когда в марте 2025 года был представлен вспомогательный инструмент, было сказано, что AuthorizationExecuteWithPrivileges (предоставление аутентификации через всплывающее окно запроса пароля) в конечном итоге будет удален, так как он уже устарел по версии Apple. Некоторые функции перестанут работать в Pearcleaner, если вспомогательный инструмент не будет включен в будущем." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Keď bol v marci 2025 predstavený pomocný nástroj, bolo povedané, že AuthorizationExecuteWithPrivileges (poskytovanie autentifikácie prostredníctvom vyskakovacieho okna s výzvou na zadanie hesla) bude nakoniec odstránené, pretože už bolo spoločnosťou Apple zrušené. Niektoré funkcie prestanú v Pearcleaner fungovať, ak pomocný nástroj nebude povolený." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ko je bilo marca 2025 uvedeno pomožno orodje, je bilo rečeno, da bo AuthorizationExecuteWithPrivileges (dodeljevanje overjanja prek pojavnega okna za vnos gesla) sčasoma odstranjeno, saj ga je Apple že opustil. Nekatere funkcije v Pearcleanerju ne bodo več delovale, če pomožno orodje ne bo omogočeno." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Yardımcı araç Mart 2025'te tanıtıldığında, AuthorizationExecuteWithPrivileges (şifre istemi açılır penceresi aracılığıyla kimlik doğrulama sağlama) işlevinin sonunda kaldırılacağı söylendi çünkü Apple tarafından zaten kullanımdan kaldırılmıştı. Yardımcı araç etkinleştirilmezse Pearcleaner'daki bazı işlevler çalışmayı durduracaktır." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Коли в березні 2025 року було представлено допоміжний інструмент, було сказано, що AuthorizationExecuteWithPrivileges (надання автентифікації через спливаюче вікно з паролем) зрештою буде видалено, оскільки його вже було знято з підтримки Apple. Деякі функції перестануть працювати в Pearcleaner, якщо допоміжний інструмент не буде увімкнено." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Khi công cụ trợ giúp được giới thiệu vào tháng 3 năm 2025, đã có thông báo rằng AuthorizationExecuteWithPrivileges (cấp quyền xác thực thông qua cửa sổ bật lên yêu cầu mật khẩu) sẽ bị loại bỏ vì nó đã bị Apple ngừng hỗ trợ. Một số chức năng sẽ ngừng hoạt động trong Pearcleaner nếu công cụ trợ giúp không được kích hoạt." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "当辅助工具在2025年3月推出时,据说AuthorizationExecuteWithPrivileges(通过密码提示弹出窗口授予身份验证)最终将被移除,因为它已经被苹果弃用。如果不启用辅助工具,Pearcleaner中的某些功能将无法继续工作。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "當輔助工具於2025年3月推出時,據說AuthorizationExecuteWithPrivileges(通過密碼提示彈出窗口授予身份驗證)最終將被移除,因為它已被Apple棄用。如果未來不啟用輔助工具,Pearcleaner中的某些功能將停止運作。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "當輔助工具於2025年3月推出時,據說AuthorizationExecuteWithPrivileges(通過密碼提示彈出窗口授權身份驗證)最終將被移除,因為它已被蘋果棄用。如果不啟用輔助工具,Pearcleaner中的某些功能將無法繼續運作。" } } } }, "When this mode is enabled, clicking the Uninstall button to remove an app will also close Pearcleaner right after.\nThis only affects Pearcleaner when it is opened via external means, like Sentinel Trash Monitor, Finder extension or a Deep Link.\nThis allows for single use of the app for a quick uninstall. When Pearcleaner is opened normally, this setting is ignored and will work as usual." : { "extractionState" : "manual", "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Wenn dieser Modus aktiviert ist, wird Pearcleaner sofort geschlossen, wenn Sie auf die Schaltfläche Deinstallieren klicken, um eine Anwendung zu entfernen.\nDies wirkt sich nur auf Pearcleaner aus, wenn es über externe Mittel wie Sentinel Trash Monitor, eine Finder-Erweiterung oder einen Deep Link geöffnet wird.\nDies ermöglicht die einmalige Verwendung der Anwendung für eine schnelle Deinstallation. Wenn Pearcleaner normal geöffnet wird, wird diese Einstellung ignoriert und funktioniert wie gewohnt." } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Cuando este modo está habilitado, al hacer clic en el botón Desinstalar para eliminar una aplicación también se cerrará Pearcleaner inmediatamente después. Esto solo afecta a Pearcleaner cuando se abre por medios externos, como Sentinel Trash Monitor, la extensión del Finder o un Enlace Directo. Esto permite un uso único de la aplicación para una desinstalación rápida. Cuando Pearcleaner se abre normalmente, esta configuración se ignora y funcionará como de costumbre." } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Lorsque ce mode est activé, cliquer sur le bouton \"Désinstaller\" pour supprimer une application fermera également Pearcleaner juste après.\nCette option est disponible seulement lorsque Pearcleaner est ouvert par des moyens externes, tels que Sentinel Trash Monitor, l'extension Finder ou un lien profond.\nCela permet une utilisation unique de l'application pour une désinstallation rapide. Lorsque Pearcleaner est ouvert normalement, ce paramètre est ignoré et fonctionne comme d'habitude." } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Ketika mode ini diaktifkan, mengklik tombol Uninstall untuk menghapus aplikasi juga akan menutup Pearcleaner segera setelahnya. Ini hanya memengaruhi Pearcleaner ketika dibuka melalui cara eksternal, seperti Sentinel Trash Monitor, ekstensi Finder, atau Deep Link. Ini memungkinkan penggunaan tunggal aplikasi untuk mencopot secara cepat. Ketika Pearcleaner dibuka secara normal, pengaturan ini diabaikan dan akan berfungsi seperti biasa." } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Quando questa modalità è abilitata, cliccando sul pulsante Disinstalla per rimuovere un'app verrà chiuso anche Pearcleaner subito dopo.\nQuesto influisce su Pearcleaner solo quando viene aperto tramite mezzi esterni, come Sentinel Trash Monitor, estensione Finder o un Deep Link.\nQuesto consente un uso singolo dell'app per una disinstallazione rapida. Quando Pearcleaner viene aperto normalmente, questa impostazione viene ignorata e funzionerà come al solito." } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "このモードが有効になっている場合、アプリを削除するために「アンインストール」ボタンをクリックすると、Pearcleanerも直後に閉じられます。\\nこれは、Sentinel Trash Monitor、Finder拡張機能、またはディープリンクなどの外部手段でPearcleanerが開かれた場合にのみ影響します。\\nこれにより、アプリを素早くアンインストールするためにアプリを1回だけ使用できます。Pearcleanerが通常の方法で開かれた場合、この設定は無視され、通常どおり機能します。" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "이 모드를 활성화하면 앱을 제거하기 위해 제거 버튼을 클릭할 때 Pearcleaner도 즉시 종료됩니다.\n이는 Sentinel Trash Monitor, Finder 확장 또는 딥 링크와 같은 외부 수단을 통해 Pearcleaner가 열렸을 때만 영향을 미칩니다.\n이를 통해 앱을 빠르게 제거하기 위한 단일 사용이 가능합니다. Pearcleaner가 일반적으로 열리면 이 설정은 무시되고 평소처럼 작동합니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Gdy ten tryb jest włączony, kliknięcie przycisku Odinstaluj w celu usunięcia aplikacji spowoduje również zamknięcie programu Pearcleaner zaraz po wykonaniu tej czynności.\nMa to wpływ tylko na program Pearcleaner, gdy jest on otwierany za pomocą zewnętrznych metod, takich jak Sentinel Trash Monitor, rozszerzenie Finder lub Deep Link.\nUmożliwia to jednorazowe użycie aplikacji w celu szybkiej dezinstalacji. Gdy program Pearcleaner jest otwierany w normalny sposób, to ustawienie jest ignorowane i działa jak zwykle." } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Quando este modo está ativado, clicar no botão Desinstalar para remover um aplicativo também fechará o Pearcleaner logo em seguida. Isso afeta somente o Pearcleaner quando é aberto por meios externos, como o Sentinel Trash Monitor, extensão do Finder ou um Deep Link. Isso permite o uso único do aplicativo para uma desinstalação rápida. Quando o Pearcleaner é aberto normalmente, essa configuração é ignorada e funcionará como de costume." } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Quando este modo está ativado, clicar no botão Desinstalar para remover uma aplicação também fechará o Pearcleaner logo em seguida.\nIsso só afeta o Pearcleaner quando ele é aberto por meios externos, como o Sentinel Trash Monitor, a extensão do Finder ou um Deep Link.\nIsso permite o uso único do aplicativo para uma desinstalação rápida. Quando o Pearcleaner é aberto normalmente, esta configuração é ignorada e funcionará como de costume." } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Когда этот режим включен, нажатие кнопки \"Удалить\" для удаления приложения также приведет к немедленному закрытию Pearcleaner.\nЭто влияет на Pearcleaner только в том случае, если оно открыто с помощью внешних средств, таких как Sentinel Trash Monitor, расширение Finder или дополнительная ссылка.\nЭто позволяет использовать приложение один раз для быстрого удаления. При обычном запуске Pearcleaner эта настройка игнорируется и будет работать как обычно." } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Keď je tento režim zapnutý, kliknutím na tlačidlo Odinštalovať sa odstránenia aplikácie a tiež okamžite zatvorí Pearcleaner. Toto platí pre Pearcleaner iba v prípade, keď sa otvorí nepriamo, napr. cez Sentinel Trash Monitor, Finder rozšírenie alebo Deep Link.\nTo umožňuje jednorazové použitie aplikácie pre rýchlu odinštaláciu. Keď sa Pearcleaner otvorí normálne, toto nastavenie sa ignoruje a bude fungovať ako obvykle." } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Ko je ta način omogočen, bo klik na gumb Odstrani za odstranitev aplikacije takoj zaprl tudi Pearcleaner.\nTo vpliva na Pearcleaner le, ko je odprt prek zunanjih sredstev, kot so Sentinel Trash Monitor, razširitev Finder ali Deep Link.\nTo omogoča enkratno uporabo aplikacije za hitro odstranitev. Ko je Pearcleaner odprt normalno, je ta nastavitev prezrta in deluje kot običajno." } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Bu mod etkinleştirildiğinde bir uygulamayı kaldırmak için Kaldır butonuna basıldığında hemen ardından Pearcleaner’da kapanacaktır. \nBu Pearcleaner’ı sadece Sentinel Trash Monitor, Finder uzantısı veya Deep Link gibi harici yöntemlerle açıldığında etkiler.\nBu, uygulamanın hızlı bir kaldırma işlemi için tek seferlik açılmasına imkan verir. Pearcleaner normal şekilde açıldığında bu ayar yok sayılır ve Pearcleaner normal şekilde çalışır." } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Якщо цей режим увімкнено, натискання кнопки Видалити для видалення програми також призведе до закриття Pearcleaner одразу після цього.\nЦе впливає на Pearcleaner лише тоді, коли його відкрито за допомогою зовнішніх засобів, таких як Sentinel Trash Monitor, розширення Finder або Deep Link.\nЦе дозволяє одноразове використання програми для швидкого видалення. Якщо Pearcleaner відкрито у звичайному режимі, цей параметр ігнорується і програма працюватиме у звичайному режимі." } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Khi bật chế độ này, việc nhấn nút Gỡ cài đặt để xóa một ứng dụng cũng sẽ tự động đóng Pearcleaner ngay sau đó.\n\nChế độ này chỉ ảnh hưởng đến Pearcleaner khi ứng dụng được mở thông qua các phương thức bên ngoài, như Sentinel Trash Monitor, tiện ích mở rộng Finder hoặc một liên kết sâu (Deep Link).\n\nTính năng này cho phép sử dụng nhanh Pearcleaner chỉ một lần để gỡ ứng dụng. Khi mở Pearcleaner theo cách thông thường, cài đặt này sẽ bị bỏ qua và ứng dụng sẽ hoạt động như bình thường." } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "当启用此模式时,单击卸载按钮以移除应用程序将立即关闭 Pearcleaner。\n这仅影响通过外部方式打开的 Pearcleaner,例如 Sentinel 垃圾监视器、Finder 扩展或深度链接。\n这允许应用程序单次使用以快速卸载。当 Pearcleaner 正常打开时,此设置将被忽略,仍然按常规方式工作。" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "啟用此模式時,按一下「解除安裝」按鈕移除應用程式之後亦會立即關閉 Pearcleaner。\n這只會影響 Pearcleaner 透過外部方式開啟󠄁,例如 Sentinel 垃圾監視器、Finder 延伸功能或深連結(Deep Link)。\n這可讓你一次性使用此應用程式以快速解除安裝。如 Pearcleaner 正常開啟,此設定將會忽略,並繼續如常工作。" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "啟用此模式時,按一下「解除安裝」按鈕移除應用程式之後亦會立即關閉 Pearcleaner。\n這只會影響 Pearcleaner 透過外部方式開啟󠄁,例如 Sentinel 垃圾監視器、Finder 延伸功能或深連結(Deep Link)。\n這可讓你一次性使用此應用程式以快速解除安裝。如 Pearcleaner 正常開啟,此設定將會忽略,並繼續如常工作。" } } } }, "Will be disabled on %@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Wird am %@ deaktiviert" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Se desactivará el %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Sera désactivé le %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Akan dinonaktifkan pada %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Sarà disabilitato il %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "%@ に無効になります" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "%@에서 비활성 될것입니다." } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Zostanie wyłączone %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Será desativado em %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Será desativado em %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Будет отключено %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Bude vypnuté %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Bo onemogočeno na %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "%@ tarihinde devre dışı bırakılacak" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Буде відключено %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Sẽ bị vô hiệu hóa vào ngày %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "将在 %@ 禁用" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "將於 %@ 停用" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "將於 %@ 停用" } } } }, "Working Directory: %@" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Arbeitsverzeichnis: %@" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Directorio de trabajo: %@" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Répertoire de travail : %@" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Direktori Kerja: %@" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Directory di Lavoro: %@" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "作業ディレクトリ: %@" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "작업 디렉토리: %@" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Katalog roboczy: %@" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Diretório de Trabalho: %@" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Diretório de Trabalho: %@" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Рабочий каталог: %@" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Pracovný adresár: %@" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Delovni imenik: %@" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Çalışma Dizini: %@" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Робочий каталог: %@" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Thư mục làm việc: %@" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "工作目录:%@" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "工作目錄:%@" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "工作目錄:%@" } } } }, "Workspace Storage Cleaner" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Arbeitsbereich-Speicherreiniger" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Limpiador de Almacenamiento del Espacio de Trabajo" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Nettoyage d’espace de travail" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Pembersih Penyimpanan Ruang Kerja" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "Pulizia Spazio di Lavoro" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "ワークスペースストレージクリーナー" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "작업 공간 저장소 정리기" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Narzędzie do czyszczenia pamięci roboczej" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Limpeza de Armazenamento do Espaço de Trabalho" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "Limpeza de Armazenamento do Espaço de Trabalho" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Очистка хранилища рабочего пространства" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Čistič úložiska pracovného priestoru" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Čistilec prostora delovnega okolja" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Çalışma Alanı Depolama Temizleyici" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Очищувач сховища робочого простору" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Trình Dọn Dẹp Bộ Nhớ Không Gian Làm Việc" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "工作区存储清理器" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "工作空間儲存清理工具" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "工作區儲存清理工具" } } } }, "Your Homebrew installation is healthy" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ihre Homebrew-Installation ist gesund" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "Tu instalación de Homebrew está saludable" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Votre installation Homebrew est en bonne santé" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Instalasi Homebrew Anda sehat" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "La tua installazione di Homebrew è sana" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew のインストールは正常です" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 설치가 정상입니다" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Twoja instalacja Homebrew jest zdrowa" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Sua instalação do Homebrew está saudável" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A sua instalação do Homebrew está saudável" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Ваша установка Homebrew работает нормально" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vaša inštalácia Homebrew je v poriadku" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Vaša namestitev Homebrew je zdrava" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew kurulumunuz sağlıklı" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ваша установка Homebrew працює нормально" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Cài đặt Homebrew của bạn đang hoạt động tốt" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "您的 Homebrew 安装运行正常" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "您的 Homebrew 安裝運行正常" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "您的 Homebrew 安裝運行正常" } } } }, "Your Homebrew installation is healthy!" : { "localizations" : { "de" : { "stringUnit" : { "state" : "translated", "value" : "Ihre Homebrew-Installation ist gesund!" } }, "es" : { "stringUnit" : { "state" : "translated", "value" : "¡Tu instalación de Homebrew está saludable!" } }, "fr" : { "stringUnit" : { "state" : "translated", "value" : "Votre installation de Homebrew est en bonne santé!" } }, "id" : { "stringUnit" : { "state" : "translated", "value" : "Instalasi Homebrew Anda sehat!" } }, "it" : { "stringUnit" : { "state" : "translated", "value" : "La tua installazione di Homebrew è a posto!" } }, "ja" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew のインストールは正常です!" } }, "ko" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew 설치가 정상입니다!" } }, "pl" : { "stringUnit" : { "state" : "translated", "value" : "Twoja instalacja Homebrew działa prawidłowo!" } }, "pt-BR" : { "stringUnit" : { "state" : "translated", "value" : "Sua instalação do Homebrew está saudável!" } }, "pt-PT" : { "stringUnit" : { "state" : "translated", "value" : "A sua instalação do Homebrew está saudável!" } }, "ru" : { "stringUnit" : { "state" : "translated", "value" : "Ваша установка Homebrew в порядке!" } }, "sk" : { "stringUnit" : { "state" : "translated", "value" : "Vaša inštalácia Homebrew je v poriadku!" } }, "sl" : { "stringUnit" : { "state" : "translated", "value" : "Vaša namestitev Homebrew je zdrava!" } }, "tr" : { "stringUnit" : { "state" : "translated", "value" : "Homebrew kurulumunuz sağlıklı!" } }, "uk" : { "stringUnit" : { "state" : "translated", "value" : "Ваша установка Homebrew працює нормально!" } }, "vi" : { "stringUnit" : { "state" : "translated", "value" : "Cài đặt Homebrew của bạn đang hoạt động tốt!" } }, "zh-Hans" : { "stringUnit" : { "state" : "translated", "value" : "您的 Homebrew 安装运行正常!" } }, "zh-Hant" : { "stringUnit" : { "state" : "translated", "value" : "您的 Homebrew 安裝運行正常!" } }, "zh-HK" : { "stringUnit" : { "state" : "translated", "value" : "您的 Homebrew 安裝運行正常!" } } } } }, "version" : "1.1" } ================================================ FILE: Pearcleaner/Resources/Pearcleaner.entitlements ================================================ com.apple.security.application-groups group.com.alienator88.Pearcleaner ================================================ FILE: Pearcleaner/Resources/askpass.sh ================================================ #!/bin/bash SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" exec "$SCRIPT_DIR/../MacOS/Pearcleaner" ask-password ================================================ FILE: Pearcleaner/Style/ChromeBorder.swift ================================================ // // ChromeBorder.swift // Pearcleaner // // Created by Alin Lupascu on 8/8/25. // import SwiftUI extension View { func chromeBorder( radius: CGFloat, highlightEnabled: Bool = true, rimEnabled: Bool = true, shadowEnabled: Bool = true, highlightIntensity: Double = 0.5, placeholderAnimationEnabled: Bool = true ) -> some View { chromeBorder( shape: RoundedRectangle(cornerRadius: radius, style: .continuous), highlightEnabled: highlightEnabled, rimEnabled: rimEnabled, shadowEnabled: shadowEnabled, highlightIntensity: highlightIntensity, placeholderAnimationEnabled: placeholderAnimationEnabled ) } func chromeBorder( shape: BorderShape, highlightEnabled: Bool = true, rimEnabled: Bool = true, shadowEnabled: Bool = true, highlightIntensity: Double = 0.5, placeholderAnimationEnabled: Bool = true ) -> some View { modifier(ChromeBorderModifier( shape: shape, highlightEnabled: highlightEnabled, rimEnabled: rimEnabled, shadowEnabled: shadowEnabled, highlightIntensity: highlightIntensity, placeholderAnimationEnabled: placeholderAnimationEnabled )) } } private struct ChromeBorderModifier: ViewModifier { var shape: BorderShape var highlightEnabled = true var rimEnabled = true var shadowEnabled = true var highlightIntensity = 0.5 var placeholderAnimationEnabled = true @State private var animate = false @Environment(\.redactionReasons) private var redaction func body(content: Content) -> some View { content .shadow(color: .black.opacity(shadowEnabled ? 0.2 : 0), radius: 6, x: 0, y: 0) .shadow(color: .black.opacity(rimEnabled ? 0.5 : 0), radius: 1, x: 0, y: 0) .overlay { if highlightEnabled { ZStack { shape .strokeBorder(Color.white, lineWidth: 1) LinearGradient(colors: [.white.opacity(0.4), .white.opacity(0.7)], startPoint: .top, endPoint: .bottom) .blendMode(.destinationOut) } .compositingGroup() .blendMode(.plusLighter) .opacity(highlightIntensity) } } .overlay { if placeholderAnimationEnabled, !redaction.isEmpty { LinearGradient(colors: [.white.opacity(0), .white.opacity(0.5), .white.opacity(0.6), .white.opacity(0.5), .white.opacity(0)], startPoint: .leading, endPoint: .trailing) .scaleEffect(x: animate ? 1 : 2, anchor: .trailing) .scaleEffect(x: animate ? 2 : 1, anchor: .leading) .clipShape(shape) .blendMode(.plusLighter) .opacity(0.2) } } .task(id: placeholderAnimationEnabled && !redaction.isEmpty) { if placeholderAnimationEnabled, !redaction.isEmpty { withAnimation(.easeInOut(duration: 5).repeatForever()) { animate.toggle() } } } } } ================================================ FILE: Pearcleaner/Style/CircularProgressView.swift ================================================ // // CircularProgressView.swift // Pearcleaner // // Circular progress indicator with thick stroke that fills clockwise // Created by Alin Lupascu on 11/20/25 // import SwiftUI import AlinFoundation struct CircularProgressView: View { let progress: Double // 0.0 to 1.0 let size: CGFloat let lineWidth: CGFloat let badgeText: String? // Optional badge overlay text (e.g., "3/5") @Environment(\.colorScheme) var colorScheme init(progress: Double, size: CGFloat, lineWidth: CGFloat, badgeText: String? = nil) { self.progress = progress self.size = size self.lineWidth = lineWidth self.badgeText = badgeText } private var accent: Color { ThemeColors.shared(for: colorScheme).accent } private var secondaryText: Color { ThemeColors.shared(for: colorScheme).secondaryText } var body: some View { ZStack { // Background circle (full ring) Circle() .stroke(secondaryText.opacity(0.3), lineWidth: lineWidth) // Progress circle (fills clockwise from top) Circle() .trim(from: 0, to: min(progress, 1.0)) .stroke(accent, style: StrokeStyle(lineWidth: lineWidth, lineCap: .round)) .rotationEffect(.degrees(-90)) // Start from top .animation(.linear(duration: 0.3), value: progress) // Optional badge text overlay if let badgeText = badgeText { Text(badgeText) .font(.system(size: size * 0.35, weight: .medium)) .foregroundStyle(secondaryText) } } .frame(width: size, height: size) } } ================================================ FILE: Pearcleaner/Style/ControlGroupChrome.swift ================================================ // // ControlGroupChrome.swift // Pearcleaner // // Created by Alin Lupascu on 8/8/25. // import SwiftUI public enum ControlGroupLevel: Int { case primary case secondary } public struct ControlGroupMetrics { static let primaryGroupRadius: Double = 14 static let secondaryGroupRadius: Double = 8 } public extension ControlGroupLevel { var cornerRadius: Double { switch self { case .primary: ControlGroupMetrics.primaryGroupRadius case .secondary: ControlGroupMetrics.secondaryGroupRadius } } } public extension View { func controlGroup(level: ControlGroupLevel = .primary) -> some View { modifier(ControlGroupChrome(level: level, shapeBuilder: { RoundedRectangle(cornerRadius: level.cornerRadius, style: .continuous) })) } func controlGroup(_ shape: S, level: ControlGroupLevel = .primary) -> some View where S: InsettableShape { modifier(ControlGroupChrome(level: level, shapeBuilder: { shape })) } } private struct ControlGroupChrome: ViewModifier { @Environment(\.colorScheme) private var colorScheme var level: ControlGroupLevel var shapeBuilder: () -> Shape var dark: Bool { colorScheme == .dark } private var innerRimOpacity: Double { switch level { case .primary: return dark ? 0.15 : 0 case .secondary: return dark ? 0.1 : 0 } } private var outerRimOpacity: Double { switch level { case .primary: return dark ? 0.5 : 0.8 case .secondary: return dark ? 0.4 : 0.7 } } private var shadowOpacity: Double { switch level { case .primary: return dark ? 0.2 : 0.1 case .secondary: return dark ? 0.1 : 0.05 } } private var material: Material { switch level { case .primary: return .thin case .secondary: return .regular } } func body(content: Content) -> some View { content .background(material, in: shape) .clipShape(shape) .shadow(color: Color.black.opacity(outerRimOpacity), radius: 1, x: 0, y: 0) .shadow(color: Color.black.opacity(shadowOpacity), radius: dark ? 8 : 10, x: 0, y: 0) .chromeBorder(shape: shape, highlightEnabled: true, rimEnabled: false, shadowEnabled: false, highlightIntensity: innerRimOpacity) .containerShape(shape) } private var shape: Shape { shapeBuilder() } } ================================================ FILE: Pearcleaner/Style/PearGroupBox.swift ================================================ // // IceGroupBox.swift // Pearcleaner // // Created by Jordan Baird (Ice), slightly altered for Pearcleaner usage by Alin Lupascu on 9/27/24. // import SwiftUI struct PearGroupBox: View { @Environment(\.colorScheme) var colorScheme @ObservedObject private var themeManager = ThemeManager.shared private let header: Header private let content: Content private let footer: Footer private let padding: CGFloat /// Example usage: /// ``` /// PearGroupBox( /// header: { Text("Header View") }, /// content: { Text("Content View") }, /// footer: { Text("Footer View") } /// ) /// ``` init( padding: CGFloat = 10, @ViewBuilder header: () -> Header, @ViewBuilder content: () -> Content, @ViewBuilder footer: () -> Footer ) { self.padding = padding self.header = header() self.content = content() self.footer = footer() } /// Example usage with no header: /// ``` /// PearGroupBox( /// content: { Text("Content View") }, /// footer: { Text("Footer View") } /// ) /// ``` init( padding: CGFloat = 10, @ViewBuilder content: () -> Content, @ViewBuilder footer: () -> Footer ) where Header == EmptyView { self.init(padding: padding) { EmptyView() } content: { content() } footer: { footer() } } /// Example usage with no footer: /// ``` /// PearGroupBox( /// header: { Text("Header View") }, /// content: { Text("Content View") } /// ) /// ``` init( padding: CGFloat = 10, @ViewBuilder header: () -> Header, @ViewBuilder content: () -> Content ) where Footer == EmptyView { self.init(padding: padding) { header() } content: { content() } footer: { EmptyView() } } /// Example usage with content only: /// ``` /// PearGroupBox { /// Text("Content View Only") /// } /// ``` init( padding: CGFloat = 10, @ViewBuilder content: () -> Content ) where Header == EmptyView, Footer == EmptyView { self.init(padding: padding) { EmptyView() } content: { content() } footer: { EmptyView() } } /// Example usage with a title and content only: /// ``` /// PearGroupBox("Header Title") { /// Text("Content View") /// } /// ``` init( _ title: LocalizedStringKey, padding: CGFloat = 10, @ViewBuilder content: () -> Content ) where Header == Text, Footer == EmptyView { self.init(padding: padding) { Text(title) .font(.headline) } content: { content() } } var body: some View { VStack(alignment: .leading) { header content .padding(padding) .background { backgroundShape .fill(ThemeColors.shared(for: colorScheme).secondaryBG) .overlay { backgroundShape .stroke(.quaternary) } } footer } } @ViewBuilder private var backgroundShape: some Shape { RoundedRectangle(cornerRadius: 7, style: .circular) } } ================================================ FILE: Pearcleaner/Style/SparkleProgressBar.swift ================================================ // // SparkleProgressBar.swift // Pearcleaner // // Animated sparkle progress bar with twinkle effects // Created by Alin Lupascu on 10/24/2025 // import SwiftUI struct SparkleProgressBar: View { let maxWidth: CGFloat let progress: Double let height: CGFloat let source: UpdateSource @Environment(\.colorScheme) var colorScheme @State private var sparkles: [SparkleConfig] = [] /// Get icon names based on update source private var iconNames: [String] { switch source { case .appStore: return ["cart", "cart.fill", "storefront", "storefront.fill"] case .sparkle: return ["star", "star.fill", "sparkle", "sparkles", "sparkles.2"] case .homebrew, .unsupported, .current: return ["star", "star.fill", "sparkle", "sparkles", "sparkles.2"] } } var body: some View { ZStack(alignment: .leading) { // Background fill (full width) Rectangle() .fill(ThemeColors.shared(for: colorScheme).accent.opacity(0.3)) .frame(width: maxWidth, height: height) // Sparkle overlay (full width, all pre-positioned) ZStack { ForEach(sparkles) { sparkle in SparkleView(config: sparkle, colorScheme: colorScheme) } } .frame(width: maxWidth, height: height) } // Mask reveals progress from left to right .mask( HStack(spacing: 0) { Rectangle() .fill(Color.white) .frame(width: maxWidth * progress) Spacer(minLength: 0) } ) .onAppear { // Generate sparkles only once when view first appears if sparkles.isEmpty { let count = max(3, Int(maxWidth / 25)) sparkles = (0.. some InsettableShape { var shape = self shape.insetAmount += amount return shape } func path(in rect: CGRect) -> Path { var path = Path() // Calculate the ladder start position - always measured from left edge let ladderStartX = rect.minX + cornerRadius + ladderPosition // Start point changes based on flip let startPoint = isFlipped ? CGPoint(x: rect.minX + cornerRadius, y: rect.minY) : CGPoint(x: rect.maxX - cornerRadius, y: rect.minY) path.move(to: startPoint) if isFlipped { // Flipped version (ladder on right) // 1. Top-left rounded corner path.addQuadCurve(to: CGPoint(x: rect.minX, y: rect.minY + cornerRadius), control: CGPoint(x: rect.minX, y: rect.minY)) // 2. Left side straight line down path.addLine(to: CGPoint(x: rect.minX, y: rect.maxY - cornerRadius)) // 3. Bottom-left rounded corner path.addQuadCurve(to: CGPoint(x: rect.minX + cornerRadius, y: rect.maxY), control: CGPoint(x: rect.minX, y: rect.maxY)) // 4. Straight line across bottom path.addLine(to: CGPoint(x: rect.maxX - cornerRadius, y: rect.maxY)) // 5. Bottom-right rounded corner path.addQuadCurve(to: CGPoint(x: rect.maxX, y: rect.maxY - cornerRadius), control: CGPoint(x: rect.maxX, y: rect.maxY)) // 6. Right side straight line going up path.addLine(to: CGPoint(x: rect.maxX, y: rect.minY + ladderHeight + cornerRadius)) // 7. Top-right rounded corner path.addQuadCurve(to: CGPoint(x: rect.maxX - cornerRadius, y: rect.minY + ladderHeight), control: CGPoint(x: rect.maxX, y: rect.minY + ladderHeight)) // 8. Straight line left to the start of the vertical section path.addLine(to: CGPoint(x: ladderStartX + cornerRadius, y: rect.minY + ladderHeight)) // 9. Curved transition into the vertical section (right side) path.addQuadCurve( to: CGPoint(x: ladderStartX, y: rect.minY + ladderHeight - cornerRadius), control: CGPoint(x: ladderStartX, y: rect.minY + ladderHeight) ) // 10. Vertical line path.addLine(to: CGPoint(x: ladderStartX, y: rect.minY + cornerRadius)) // 11. Curved transition from vertical to top (left side) path.addQuadCurve( to: CGPoint(x: ladderStartX - cornerRadius, y: rect.minY), control: CGPoint(x: ladderStartX, y: rect.minY) ) // 12. Final line to close the shape path.addLine(to: CGPoint(x: rect.minX + cornerRadius, y: rect.minY)) } else { // Original version (ladder on left) - keeping your original code and comments // 1. Top-right rounded corner path.addQuadCurve(to: CGPoint(x: rect.maxX, y: rect.minY + cornerRadius), control: CGPoint(x: rect.maxX, y: rect.minY)) // 2. Right side straight line down path.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - cornerRadius)) // 3. Bottom-right rounded corner path.addQuadCurve(to: CGPoint(x: rect.maxX - cornerRadius, y: rect.maxY), control: CGPoint(x: rect.maxX, y: rect.maxY)) // 4. Straight line across bottom path.addLine(to: CGPoint(x: rect.minX + cornerRadius, y: rect.maxY)) // 5. Bottom-left rounded corner path.addQuadCurve(to: CGPoint(x: rect.minX, y: rect.maxY - cornerRadius), control: CGPoint(x: rect.minX, y: rect.maxY)) // 6. Left side straight line going up path.addLine(to: CGPoint(x: rect.minX, y: rect.minY + ladderHeight + cornerRadius)) // 7. Top-left rounded corner path.addQuadCurve(to: CGPoint(x: rect.minX + cornerRadius, y: rect.minY + ladderHeight), control: CGPoint(x: rect.minX, y: rect.minY + ladderHeight)) // 8. Straight line right to the start of the vertical section path.addLine(to: CGPoint(x: ladderStartX - cornerRadius, y: rect.minY + ladderHeight)) // 9. Curved transition into the vertical section (left side) path.addQuadCurve( to: CGPoint(x: ladderStartX, y: rect.minY + ladderHeight - cornerRadius), control: CGPoint(x: ladderStartX, y: rect.minY + ladderHeight) ) // 10. Vertical line path.addLine(to: CGPoint(x: ladderStartX, y: rect.minY + cornerRadius)) // 11. Curved transition from vertical to top (right side) path.addQuadCurve( to: CGPoint(x: ladderStartX + cornerRadius, y: rect.minY), control: CGPoint(x: ladderStartX, y: rect.minY) ) // 12. Final line to close the shape path.addLine(to: CGPoint(x: rect.maxX - cornerRadius, y: rect.minY)) } path.closeSubpath() return path } } struct ResetSettingsButtonStyle: ButtonStyle { @Environment(\.colorScheme) var colorScheme @Binding var isResetting: Bool let label: String let help: String @State private var hovered = false @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true func makeBody(configuration: Configuration) -> some View { HStack(alignment: .center) { if isResetting { ProgressView() .progressViewStyle(CircularProgressViewStyle()) .controlSize(.small) .frame(width: 15) } else { Image(systemName: "gear") .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText).padding(.leading, 2).padding(.trailing, 0) } Text(label) .textCase(.uppercase).foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText).fontWeight(.bold).padding(.trailing, 2) } .padding(8) .background(!configuration.isPressed ? hovered ? ThemeColors.shared(for: colorScheme).primaryText.opacity(0.4) : ThemeColors.shared(for: colorScheme).primaryText.opacity(0) : ThemeColors.shared(for: colorScheme).primaryText.opacity(0.5)) .cornerRadius(6) .scaleEffect(configuration.isPressed ? 0.95 : 1) .animation(.easeOut(duration: 0.2), value: configuration.isPressed) .onHover { hovering in withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { hovered = hovering } } .overlay( RoundedRectangle(cornerRadius: 6) .strokeBorder(ThemeColors.shared(for: colorScheme).primaryText.opacity(0.5), lineWidth: 1) .scaleEffect(configuration.isPressed ? 0.95 : 1) ) .help(help) } } struct SettingsControlButtonGroup: View { @Environment(\.colorScheme) var colorScheme @Binding var isResetting: Bool let resetAction: () -> Void let exportAction: () -> Void let importAction: () -> Void @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true var body: some View { HStack(spacing: 10) { Image(systemName: "gear") .resizable() .scaledToFit() .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) .frame(width: 20) .rotationEffect(isResetting ? .degrees(360) : .degrees(0)) .animation(isResetting ? .linear(duration: 1).repeatForever(autoreverses: false) : .default, value: isResetting) ForEach(0..<3) { index in let (label, action): (String, () -> Void) = switch index { case 0: ("Reset", resetAction) case 1: ("Export", exportAction) default: ("Import", importAction) } Button(action: action) { Text(label) .textCase(.uppercase) .fontWeight(.bold) } if index < 2 { Divider().frame(height: 10) } } } .disabled(isResetting) .controlSize(.small) .buttonStyle(.plain) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) .padding(.vertical, 8) .padding(.horizontal, 14) .controlGroup(Capsule(style: .continuous), level: .primary) } } struct ControlGroupButtonStyle: ButtonStyle { let foregroundColor: Color let shape: Shape let level: ControlGroupLevel let verticalPadding: CGFloat let horizontalPadding: CGFloat let skipControlGroup: Bool let disabled: Bool init( foregroundColor: Color, shape: Shape, level: ControlGroupLevel = .secondary, verticalPadding: CGFloat = 8, horizontalPadding: CGFloat = 14, skipControlGroup: Bool = false, disabled: Bool = false ) { self.foregroundColor = foregroundColor self.shape = shape self.level = level self.verticalPadding = verticalPadding self.horizontalPadding = horizontalPadding self.skipControlGroup = skipControlGroup self.disabled = disabled } func makeBody(configuration: Self.Configuration) -> some View { let styledContent = configuration.label .foregroundStyle(foregroundColor) .opacity(disabled ? 0.5 : 1.0) // 50% opacity when disabled .controlSize(.small) .padding(.vertical, verticalPadding) .padding(.horizontal, horizontalPadding) // .scaleEffect(configuration.isPressed && !disabled ? 0.98 : 1.0) // Only scale if not disabled // .animation(.easeInOut(duration: 0.1), value: configuration.isPressed) if skipControlGroup { styledContent } else { styledContent.controlGroup(shape, level: level) } } } struct SimpleCheckboxToggleStyle: ToggleStyle { @Environment(\.colorScheme) var colorScheme @State private var isHovered: Bool = false @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true @AppStorage("settings.general.glass") private var glass: Bool = false func makeBody(configuration: Configuration) -> some View { HStack { RoundedRectangle(cornerRadius: 4) .fill(Color.white.opacity(0.0000000001)) .frame(width: 14, height: 14) .overlay { if configuration.isOn { Image(systemName: "checkmark") .resizable() .aspectRatio(contentMode: .fit) .frame(width: 8, height: 8) .foregroundStyle(!isHovered ? ThemeColors.shared(for: colorScheme).primaryText : ThemeColors.shared(for: colorScheme).secondaryText) } } .overlay { RoundedRectangle(cornerRadius: 4) .strokeBorder(ThemeColors.shared(for: colorScheme).secondaryText.opacity(0.5), lineWidth: 2) } .onTapGesture { withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { configuration.isOn.toggle() } } configuration.label } .onHover(perform: { hovering in self.isHovered = hovering }) } } struct CircleCheckboxToggleStyle: ToggleStyle { @Environment(\.colorScheme) var colorScheme func makeBody(configuration: Configuration) -> some View { Button(action: { configuration.isOn.toggle() }) { HStack(spacing: 8) { Image(systemName: configuration.isOn ? "checkmark.circle.fill" : "circle") .foregroundStyle(configuration.isOn ? ThemeColors.shared(for: colorScheme).accent : ThemeColors.shared(for: colorScheme).secondaryText) .font(.title3) configuration.label } } .buttonStyle(.plain) } } struct CircleCheckboxButtonStyle: ButtonStyle { let isSelected: Bool @Environment(\.colorScheme) var colorScheme func makeBody(configuration: Configuration) -> some View { HStack(spacing: 8) { Image(systemName: isSelected ? "checkmark.circle.fill" : "circle") .foregroundStyle(isSelected ? ThemeColors.shared(for: colorScheme).accent : ThemeColors.shared(for: colorScheme).secondaryText) .font(.title3) configuration.label } .opacity(configuration.isPressed ? 0.7 : 1.0) } } public struct SlideableDivider: View { @Binding var dimension: Double @State private var color: Color @State private var dimensionStart: Double? @State private var handleWidth: Double = 4 @State private var handleHeight: Double = 30 @State private var isHovered: Bool = false public init(dimension: Binding, color: Color = .primary ) { self._dimension = dimension self.color = color } public var body: some View { Divider() .foregroundStyle(color) .background( // Invisible wider hover area Rectangle() .fill(Color.clear) .frame(width: 10) // 10pt wide hover area .contentShape(Rectangle()) // Makes the clear area interactive ) .onHover { inside in if inside { NSCursor.resizeLeftRight.push() } else { NSCursor.pop() } } .contextMenu { Button("Reset Size") { dimension = 265 } } .gesture(drag) .help("Right click to reset size") .ignoresSafeArea(.all) } var drag: some Gesture { DragGesture(minimumDistance: 5, coordinateSpace: CoordinateSpace.global) .onChanged { val in if dimensionStart == nil { dimensionStart = dimension } let delta = val.location.x - val.startLocation.x let newDimension = dimensionStart! + Double(delta) // Set minimum and maximum width let minWidth: Double = 240 let maxWidth: Double = 300 dimension = max(minWidth, min(maxWidth, newDimension)) NSCursor.closedHand.set() handleWidth = 6 handleHeight = 40 } .onEnded { val in dimensionStart = nil NSCursor.arrow.set() handleWidth = 4 handleHeight = 30 } } } struct RoundedTextFieldStyle: TextFieldStyle { @Environment(\.colorScheme) var colorScheme @FocusState private var isFocused func _body(configuration: TextField) -> some View { configuration .padding(8) .cornerRadius(6) .textFieldStyle(.plain) .overlay( RoundedRectangle(cornerRadius: 8) .strokeBorder(ThemeColors.shared(for: colorScheme).secondaryText, lineWidth: 0.8) ) .focused($isFocused) .onAppear { updateOnMain { isFocused = false } } } } struct GlassEffect: NSViewRepresentable { var material: NSVisualEffectView.Material // Choose the material you want var blendingMode: NSVisualEffectView.BlendingMode // Choose the blending mode func makeNSView(context: Self.Context) -> NSView { let visualEffectView = NSVisualEffectView() visualEffectView.material = material visualEffectView.blendingMode = blendingMode return visualEffectView } func updateNSView(_ nsView: NSView, context: Context) { } } struct SimpleButtonBrightStyle: ButtonStyle { @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true @State private var hovered = false let icon: String let label: String let help: String let color: Color let shield: Bool? init(icon: String, label: String = "", help: String, color: Color, shield: Bool? = nil) { self.icon = icon self.label = label self.help = help self.color = color self.shield = shield } func makeBody(configuration: Self.Configuration) -> some View { HStack { Image(systemName: icon) .resizable() .scaledToFit() .frame(width: 20) .foregroundStyle(hovered ? color.opacity(0.5) : color) Text(label) } .padding(5) .onHover { hovering in withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { hovered = hovering } } .scaleEffect(configuration.isPressed ? 0.95 : 1) .help(help) } } public struct SimpleButtonStyleFlipped: ButtonStyle { @Environment(\.colorScheme) var colorScheme @State private var hovered = false let icon: String let iconFlip: String let label: String let help: String let color: Color let size: CGFloat let padding: CGFloat let rotate: Bool public init(icon: String, iconFlip: String = "", label: String = "", help: String, color: Color = .primary, size: CGFloat = 20, padding: CGFloat = 5, rotate: Bool = false) { self.icon = icon self.iconFlip = iconFlip self.label = label self.help = help self.color = color self.size = size self.padding = padding self.rotate = rotate } public func makeBody(configuration: Self.Configuration) -> some View { HStack(alignment: .center) { if !label.isEmpty { Text(label) } Image(systemName: (hovered && !iconFlip.isEmpty) ? iconFlip : icon) .resizable() .scaledToFit() .frame(width: size, height: size) .rotationEffect(.degrees(rotate ? (hovered ? 90 : 0) : 0)) .animation(.easeInOut(duration: 0.2), value: hovered) } .foregroundStyle(hovered ? color.opacity(0.5) : color) .padding(padding) .onHover { hovering in withAnimation() { hovered = hovering } } .scaleEffect(configuration.isPressed ? 0.90 : 1) .help(help) } } // Background color/glass setter @ViewBuilder func backgroundView(color: Color, glass: Bool = false) -> some View { if glass { GlassEffect(material: .sidebar, blendingMode: .behindWindow) .edgesIgnoringSafeArea(.all) } else { color.edgesIgnoringSafeArea(.all) } } struct preTahoeSidebar: ViewModifier { @Environment(\.colorScheme) var colorScheme @AppStorage("settings.general.glass") private var glass: Bool = false func body(content: Content) -> some View { if #available(macOS 26.0, *) { content .background { LinearGradient(colors: [ThemeColors.shared(for: colorScheme).secondaryBG, ThemeColors.shared(for: colorScheme).primaryBG], startPoint: .leading, endPoint: .trailing) } } else { content .background(backgroundView(color: ThemeColors.shared(for: colorScheme).secondaryBG, glass: glass)) .overlay( Rectangle() .fill(ThemeColors.shared(for: colorScheme).primaryText.opacity(0.1)) .frame(width: 1) .frame(maxWidth: .infinity, alignment: .trailing) ) } } } extension View { func preTahoeSidebarBG() -> some View { self.modifier(preTahoeSidebar()) } } struct TahoeToolbarItem: ToolbarContent { var id: String? = nil var placement: ToolbarItemPlacement = .automatic var isGroup: Bool = false @ViewBuilder let content: () -> Content var body: some ToolbarContent { if isGroup { if #available(macOS 26.0, *) { ToolbarItemGroup(placement: placement) { content() } .sharedBackgroundVisibility(.hidden) } else { ToolbarItemGroup(placement: placement) { content() } } } else { if #available(macOS 26.0, *) { if let id { ToolbarItem(id: id, placement: placement) { content() } .sharedBackgroundVisibility(.hidden) } else { ToolbarItem(placement: placement) { content() } .sharedBackgroundVisibility(.hidden) } } else { if let id { ToolbarItem(id: id, placement: placement) { content() } } else { ToolbarItem(placement: placement) { content() } } } } } } struct ifGlassAvailable: ViewModifier { @AppStorage("settings.general.glassEffect") private var glassEffect: String = "Regular" func body(content: Content) -> some View { if #available(macOS 26.0, *) { content .glassEffect(glassEffect == "Regular" ? .regular : .clear, in: .rect(cornerRadius: 20)) } else { content } } } extension View { func ifGlass() -> some View { self.modifier(ifGlassAvailable()) } } struct ifGlassAvailableMain: ViewModifier { @AppStorage("settings.general.glassEffect") private var glassEffect: String = "Regular" @AppStorage("settings.general.glass") private var glass: Bool = false @Environment(\.colorScheme) var colorScheme func body(content: Content) -> some View { if #available(macOS 26.0, *) { content .glassEffect(glassEffect == "Regular" ? .regular : .clear, in: .rect(cornerRadius: 20)) } else { content .background(backgroundView(color: ThemeColors.shared(for: colorScheme).secondaryBG, glass: glass)) .clipShape(RoundedRectangle(cornerRadius: 8)) .overlay { RoundedRectangle(cornerRadius: 8) .strokeBorder(ThemeColors.shared(for: colorScheme).primaryText.opacity(0.2), lineWidth: 1) } } } } extension View { func ifGlassMain() -> some View { self.modifier(ifGlassAvailableMain()) } } struct ifGlassAvailableSidebar: ViewModifier { @Environment(\.colorScheme) var colorScheme @AppStorage("settings.general.glassEffect") private var glassEffect: String = "Regular" func body(content: Content) -> some View { if #available(macOS 26.0, *) { content .background(.ultraThinMaterial.opacity(glassEffect == "Regular" ? 0 : 0.7)) .glassEffect(glassEffect == "Regular" ? .regular : .clear, in: .rect(cornerRadius: 20)) .clipShape(RoundedRectangle(cornerRadius: 20)) .overlay { RoundedRectangle(cornerRadius: 20) .strokeBorder(ThemeColors.shared(for: colorScheme).primaryText.opacity(0.2), lineWidth: colorScheme == .light ? 1 : 0) } } else { content .background(.ultraThinMaterial) .clipShape(RoundedRectangle(cornerRadius: 8)) .overlay { RoundedRectangle(cornerRadius: 8) .strokeBorder(ThemeColors.shared(for: colorScheme).primaryText.opacity(0.2), lineWidth: 1) } } } } extension View { func ifGlassSidebar() -> some View { self.modifier(ifGlassAvailableSidebar()) } } struct SettingsToggle: ToggleStyle { @Environment(\.colorScheme) var colorScheme func makeBody(configuration: Configuration) -> some View { Button { configuration.isOn.toggle() } label: { ZStack { // Background Track Capsule() .fill(.tertiary.opacity(0.5)) .frame(width: 40, height: 24) // Toggle Knob Circle() .fill(configuration.isOn ? ThemeColors.shared(for: colorScheme).accent : ThemeColors.shared(for: colorScheme).secondaryText) .frame(width: 18, height: 18) .offset(x: configuration.isOn ? 8 : -8) .animation(.spring(duration: 0.2), value: configuration.isOn) } } .buttonStyle(.plain) } } // MARK: - View Extension for Conditional Modifiers extension View { /// Helper to conditionally apply modifiers func apply(@ViewBuilder _ transform: (Self) -> Content) -> Content { transform(self) } } //struct HelperBadge: View { // @EnvironmentObject var updater: Updater // // var body: some View { // AlertNotification(label: String(localized:"Helper Not Installed"), icon: "key", buttonAction: { // openAppSettingsWindow(tab: .helper, updater: updater) // }, btnColor: Color.orange, hideLabel: false) // // } //} struct SimpleSearchStyleSidebar: TextFieldStyle { @Environment(\.colorScheme) var colorScheme @State private var isHovered = false @FocusState private var isFocused: Bool @State var menu: Bool = true @State var trash: Bool = false @Binding var text: String @EnvironmentObject var appState: AppState @EnvironmentObject var fsm: FolderSettingsManager @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true @AppStorage("settings.general.selectedSortAppsList") var selectedSortOption: SortOption = .alphabetical @AppStorage("settings.interface.multiSelect") private var multiSelect: Bool = false @AppStorage("settings.general.sidebarWidth") private var sidebarWidth: Double = 265 func _body(configuration: TextField) -> some View { HStack { configuration .font(.title3) .textFieldStyle(PlainTextFieldStyle()) Spacer() if trash && text != "" { Button { text = "" } label: { Image(systemName: "delete.left.fill") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } if menu { Menu { Section(header: Text("Sorting")) { ForEach(SortOption.allCases) { option in Button { withAnimation( Animation.easeInOut(duration: animationEnabled ? 0.35 : 0) ) { selectedSortOption = option } } label: { HStack { Image( systemName: selectedSortOption == option ? "circle.inset.filled" : "circle") Text(option.title) } } } } Section(header: Text("Layout")) { Button(action: { withAnimation(.spring(duration: animationEnabled ? 0.3 : 0)) { sidebarWidth = 265 } }) { HStack { Image(systemName: sidebarWidth < 316 ? "circle.inset.filled" : "circle") Text("List View") } } Button(action: { withAnimation(.spring(duration: animationEnabled ? 0.3 : 0)) { sidebarWidth = 375 } }) { HStack { Image(systemName: sidebarWidth > 316 ? "circle.inset.filled" : "circle") Text("Grid View") } } } Section(header: Text("Options")) { Button("Refresh List") { Task { @MainActor in withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { // Use default non-streaming mode for manual refresh (needs full AppInfo) loadApps(folderPaths: fsm.folderPaths) } } } Button(multiSelect ? "Hide multi-select" : "Show multi-select") { withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { multiSelect.toggle() } } } } label: { Image(systemName: "line.3.horizontal") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .padding(2) .contentShape(Rectangle()) } .menuStyle(BorderlessButtonMenuStyle()) .menuIndicator(.hidden) .frame(width: 16) } } .buttonStyle(.plain) .padding(.vertical, 8) .padding(.horizontal, 14) .controlGroup(Capsule(style: .continuous), level: .primary) .onHover { hovering in withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { self.isHovered = hovering self.isFocused = true } } .focused($isFocused) .onAppear { updateOnMain { self.isFocused = true } } } } // Hide blinking textfield caret extension NSTextView { open override var frame: CGRect { didSet { insertionPointColor = NSColor(.primary.opacity(0.2)) //.clear } } } struct BetaBadge: View { let label: String let fontSize: CGFloat init(label: String = "BETA", fontSize: CGFloat = 10) { self.label = label self.fontSize = fontSize } var body: some View { Text(label).font(.system(size: fontSize)).foregroundStyle(.orange) .padding(1).padding(.horizontal, 2) .overlay { RoundedRectangle(cornerRadius: 4) .strokeBorder(.orange, lineWidth: 1) } } } struct ProgressStepView: View { var currentStep: Int @Namespace private var animation @Environment(\.colorScheme) var colorScheme @AppStorage("settings.interface.animationEnabled") var animationEnabled: Bool = true @AppStorage("settings.general.spotlight") var spotlight = true var body: some View { VStack(spacing: 4) { if currentStep > 0 && spotlight { HStack(spacing: 8) { Text("Searching:") .font(.title2) .opacity(0) Text("File System") .font(.title2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText.opacity(0.5)) .matchedGeometryEffect(id: "previous", in: animation) .transition(.move(edge: .top).combined(with: .opacity)) } } HStack(spacing: 8) { Text("Searching:") .font(.title2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text((currentStep == 0 || !spotlight) ? "File System" : "Spotlight Index") .font(.title2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .matchedGeometryEffect(id: "current", in: animation) .transition(.opacity) ProgressView().controlSize(.small) } .animation(.easeInOut(duration: animationEnabled ? 0.3 : 0), value: currentStep) } .frame(height: 48) } } // MARK: - File Drop Handler Extension extension View { /// Handle file drop operations and route to deeplink manager func handleFileDrop( updater: Updater, fsm: FolderSettingsManager, appState: AppState, locations: Locations, isTargeted: Binding ) -> some View { self.onDrop(of: ["public.file-url"], isTargeted: isTargeted) { providers, _ in var droppedURLs: [URL] = [] let dispatchGroup = DispatchGroup() for provider in providers { dispatchGroup.enter() provider.loadItem(forTypeIdentifier: "public.file-url") { data, error in if let data = data as? Data, let url = URL(dataRepresentation: data, relativeTo: nil) { droppedURLs.append(url) } dispatchGroup.leave() } } dispatchGroup.notify(queue: .main) { let deeplinkManager = DeeplinkManager(updater: updater, fsm: fsm) for url in droppedURLs { deeplinkManager.manage(url: url, appState: appState, locations: locations) } } return true } } } // MARK: - Collapsible GroupBox Style struct CollapsibleGroupBoxStyle: GroupBoxStyle { let icon: String? let title: String let count: Int let isCollapsed: Bool let isLoading: Bool let onToggle: () -> Void @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true @Environment(\.colorScheme) var colorScheme init( icon: String? = nil, title: String, count: Int, isCollapsed: Bool, isLoading: Bool = false, onToggle: @escaping () -> Void ) { self.icon = icon self.title = title self.count = count self.isCollapsed = isCollapsed self.isLoading = isLoading self.onToggle = onToggle } func makeBody(configuration: Configuration) -> some View { VStack(alignment: .leading, spacing: 8) { Button(action: { withAnimation(.easeInOut(duration: animationEnabled ? 0.3 : 0)) { onToggle() } }) { HStack { Image(systemName: isCollapsed ? "chevron.right" : "chevron.down") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .frame(width: 10) if let icon = icon { Image(systemName: icon) .font(.headline) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) .frame(width: 20) } Text(title) .font(.headline) .fontWeight(.semibold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) if isLoading { ProgressView() .controlSize(.small) } else { Text(verbatim: "(\(count))") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } Spacer() } } .buttonStyle(.plain) configuration.content } .frame(maxWidth: .infinity) .background(Color.clear) } } extension GroupBoxStyle where Self == CollapsibleGroupBoxStyle { static func collapsible( icon: String? = nil, title: String, count: Int, isCollapsed: Bool, isLoading: Bool = false, onToggle: @escaping () -> Void ) -> CollapsibleGroupBoxStyle { CollapsibleGroupBoxStyle( icon: icon, title: title, count: count, isCollapsed: isCollapsed, isLoading: isLoading, onToggle: onToggle ) } } ================================================ FILE: Pearcleaner/Style/Theme.swift ================================================ import SwiftUI import AlinFoundation // MARK: - Observable Theme Manager class ThemeManager: ObservableObject { static let shared = ThemeManager() @AppStorage("settings.interface.customDarkColors") var customDarkColorsString: String = "" @AppStorage("settings.interface.customLightColors") var customLightColorsString: String = "" private init() {} func saveCustomColors(dark: String, light: String) { customDarkColorsString = dark customLightColorsString = light } func resetToDefaults() { customDarkColorsString = "" customLightColorsString = "" } } // MARK: - ThemeColors Struct // MARK: - Example Usage // Use this anywhere in your SwiftUI views: // // struct ExampleView: View { // var body: some View { // Text("App Name") // .foregroundStyle(theme(for: colorScheme).textPrimary) // .padding() // .background(theme(for: colorScheme).backgroundPanel) // } // } struct ThemeColors { static func shared(for colorScheme: ColorScheme) -> ThemeColors { ThemeColors(colorScheme: colorScheme) } private let colorScheme: ColorScheme private var isDark: Bool { colorScheme == .dark } // MARK: - Default Color Values - Made accessible static let defaultDarkColors = [ "#1a1b1f", // backgroundMain "#2d2e33", // backgroundPanel "#ffffff", // textPrimary "#929292", // textSecondary "#419cff" // accentPrimary ] static let defaultLightColors = [ "#fdfcfa", // backgroundMain "#f6f5f2", // backgroundPanel "#1e1e1e", // textPrimary "#676767", // textSecondary "#0068da" // accentPrimary ] // MARK: - Custom Color Loading (now uses ThemeManager) private var currentColors: [String] { let themeManager = ThemeManager.shared let customString = isDark ? themeManager.customDarkColorsString : themeManager.customLightColorsString let defaultColors = isDark ? Self.defaultDarkColors : Self.defaultLightColors if customString.isEmpty { return defaultColors } let customColors = customString.components(separatedBy: ",").map { $0.trimmingCharacters(in: .whitespaces) } // Ensure we have exactly 5 colors, fall back to defaults for missing ones var colors: [String] = [] for i in 0..<5 { if i < customColors.count && !customColors[i].isEmpty { colors.append(customColors[i]) } else { colors.append(defaultColors[i]) } } return colors } // Main window background (overall background of the app) var primaryBG: Color { Color(hex: currentColors[0]) } // Sidebar or secondary panel background var secondaryBG: Color { Color(hex: currentColors[1]) } // Main text color (titles, app names) var primaryText: Color { Color(hex: currentColors[2]) } // Secondary text (dates, file sizes, subtle info) var secondaryText: Color { Color(hex: currentColors[3]) } // Primary accent (e.g. branding highlight, app icon pink) var accent: Color { Color(hex: currentColors[4]) } // MARK: - Static Methods for Theme Management (now use ThemeManager) static func saveCustomColors(dark: String, light: String) { ThemeManager.shared.saveCustomColors(dark: dark, light: light) } static func resetToDefaults() { ThemeManager.shared.resetToDefaults() } static func getCurrentColorsString(for colorScheme: ColorScheme) -> String { let themeManager = ThemeManager.shared let isDark = colorScheme == .dark let customString = isDark ? themeManager.customDarkColorsString : themeManager.customLightColorsString let defaultColors = isDark ? defaultDarkColors : defaultLightColors if customString.isEmpty { return defaultColors.joined(separator: ", ") } return customString } static func getDefaultColorsString(for colorScheme: ColorScheme) -> String { let defaultColors = colorScheme == .dark ? defaultDarkColors : defaultLightColors return defaultColors.joined(separator: ", ") } } // MARK: - View Extension for Easy Access extension View { func theme(for colorScheme: ColorScheme) -> ThemeColors { return ThemeColors.shared(for: colorScheme) } } // MARK: - Color Extension for Hex Support extension Color { init(hex: String) { let hex = hex.trimmingCharacters(in: .alphanumerics.inverted) var int: UInt64 = 0 Scanner(string: hex).scanHexInt64(&int) let r = Double((int >> 16) & 0xFF) / 255 let g = Double((int >> 8) & 0xFF) / 255 let b = Double(int & 0xFF) / 255 self.init(red: r, green: g, blue: b) } var hexString: String { let components = NSColor(self).cgColor.components ?? [0, 0, 0, 1] let r = Int(components[0] * 255) let g = Int(components[1] * 255) let b = Int(components[2] * 255) return String(format: "#%02x%02x%02x", r, g, b) } func adjust(brightness: Double) -> Color { let components = NSColor(self).cgColor.components ?? [0, 0, 0, 1] let r = min(max(components[0] + brightness, 0), 1) let g = min(max(components[1] + brightness, 0), 1) let b = min(max(components[2] + brightness, 0), 1) let a = components[3] return Color(red: r, green: g, blue: b, opacity: a) } } struct ColorSquare: View { let color: Color let name: String @Environment(\.colorScheme) var colorScheme var body: some View { VStack(spacing: 4) { Rectangle() .fill(color) .frame(width: 30, height: 30) .cornerRadius(6) .overlay( RoundedRectangle(cornerRadius: 6) .stroke(ThemeColors.shared(for: colorScheme).secondaryText, lineWidth: 1) ) Text(name) .font(.caption2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .frame(width: 80) } } struct ThemeColorDemo: View { @Environment(\.colorScheme) var colorScheme @ObservedObject private var themeManager = ThemeManager.shared private var theme: ThemeColors { ThemeColors.shared(for: colorScheme) } var body: some View { HStack(spacing: 8) { ColorSquare(color: theme.primaryBG, name: "Primary BG") ColorSquare(color: theme.secondaryBG, name: "Secondary BG") ColorSquare(color: theme.primaryText, name: "Primary Text") ColorSquare(color: theme.secondaryText, name: "Secondary Text") ColorSquare(color: theme.accent, name: "Accent") } .frame(maxWidth: .infinity, alignment: .center) } } struct CustomColorPicker: View { @Binding var selectedColor: Color @State private var hue: Double = 0.0 @State private var saturation: Double = 1.0 @State private var brightness: Double = 1.0 let pickerSize: CGSize = CGSize(width: 280, height: 200) let sliderHeight: CGFloat = 20 // Pre-compute the hue gradient colors to avoid recalculation private let hueGradientColors = [ Color(hue: 0, saturation: 1, brightness: 1), Color(hue: 0.16, saturation: 1, brightness: 1), Color(hue: 0.33, saturation: 1, brightness: 1), Color(hue: 0.5, saturation: 1, brightness: 1), Color(hue: 0.66, saturation: 1, brightness: 1), Color(hue: 0.83, saturation: 1, brightness: 1), Color(hue: 1, saturation: 1, brightness: 1) ] var body: some View { VStack(spacing: 16) { // Color Gradient with better corner handling ZStack { LinearGradient( gradient: Gradient(colors: [ Color(hue: hue, saturation: 0, brightness: 1), Color(hue: hue, saturation: 1, brightness: 1) ]), startPoint: .leading, endPoint: .trailing ) LinearGradient( gradient: Gradient(colors: [ Color.clear, Color.black ]), startPoint: .top, endPoint: .bottom ) .blendMode(.multiply) // This should help with corner blending // Scope image Image(systemName: "scope") .resizable() .scaledToFit() .frame(width: 16, height: 16) .shadow(color: Color.black, radius: 2) .position( x: CGFloat(saturation) * pickerSize.width - 1, y: CGFloat(1 - brightness) * pickerSize.height - 1 ) .allowsHitTesting(false) } .frame(width: pickerSize.width, height: pickerSize.height) .drawingGroup() // This can help with rendering artifacts .clipShape(RoundedRectangle(cornerRadius: 8)) // .overlay( // RoundedRectangle(cornerRadius: 8) // .stroke(Color.white, lineWidth: 0.8) // ) .contentShape(RoundedRectangle(cornerRadius: 8)) .gesture( DragGesture(minimumDistance: 0) .onChanged { value in updateSaturationBrightness(from: value.location) } ) // Hue gradient ZStack(alignment: .leading) { LinearGradient( gradient: Gradient(colors: hueGradientColors), startPoint: .leading, endPoint: .trailing ) .frame(width: pickerSize.width, height: sliderHeight) // Vertical rounded rectangle thumb RoundedRectangle(cornerRadius: 3) .fill(Color.white) .frame(width: 2, height: sliderHeight - 2) .shadow(color: Color.black, radius: 2) .offset(x: CGFloat(hue) * pickerSize.width - 3) } .clipShape(RoundedRectangle(cornerRadius: 6)) // .overlay( // RoundedRectangle(cornerRadius: 8) // .stroke(Color.white, lineWidth: 0.8) // ) .contentShape(RoundedRectangle(cornerRadius: 6)) .gesture( DragGesture(minimumDistance: 0) .onChanged { value in updateHue(from: value.location) } ) } .onAppear { initializeFromColor() } } private func updateSaturationBrightness(from location: CGPoint) { // Ensure the values are properly bounded and mapped saturation = Double(min(max(location.x / pickerSize.width, 0), 1)) brightness = Double(min(max(1 - (location.y / pickerSize.height), 0), 1)) updateSelectedColor() } private func updateHue(from location: CGPoint) { hue = Double(min(max(location.x / pickerSize.width, 0), 1)) updateSelectedColor() } private func updateSelectedColor() { selectedColor = Color(hue: hue, saturation: saturation, brightness: brightness) } private func initializeFromColor() { let uiColor = NSColor(selectedColor) var h: CGFloat = 0, s: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0 uiColor.getHue(&h, saturation: &s, brightness: &b, alpha: &a) hue = Double(h) saturation = Double(s) brightness = Double(b) } } struct ClickableColorSquare: View { let name: String let colorIndex: Int @Binding var colorsText: String @Environment(\.colorScheme) var colorScheme @ObservedObject private var themeManager = ThemeManager.shared @State private var showingColorPicker = false @State private var selectedColor: Color = .clear // Computed property to get the current color from theme (no local override) private var currentColor: Color { let theme = ThemeColors.shared(for: colorScheme) switch colorIndex { case 0: return theme.primaryBG case 1: return theme.secondaryBG case 2: return theme.primaryText case 3: return theme.secondaryText case 4: return theme.accent default: return .clear } } var body: some View { VStack(spacing: 4) { Rectangle() .fill(currentColor) // Always uses theme color, no local override .frame(width: 30, height: 30) .cornerRadius(6) .overlay( RoundedRectangle(cornerRadius: 6) .stroke(ThemeColors.shared(for: colorScheme).secondaryText, lineWidth: 1) ) .onTapGesture { selectedColor = currentColor showingColorPicker = true } .popover(isPresented: $showingColorPicker, arrowEdge: .top) { VStack(spacing: 16) { // Large color preview background - this updates dynamically // RoundedRectangle(cornerRadius: 12) // .fill(selectedColor) // .frame(height: 60) // .overlay( // RoundedRectangle(cornerRadius: 12) // .stroke(ThemeColors.shared(for: colorScheme).secondaryText.opacity(0.3), lineWidth: 1) // ) // .padding(.horizontal) CustomColorPicker(selectedColor: $selectedColor) // No onChange needed since we're not updating anything locally Button("Save") { // Save to theme when closing updateColorInTheme() showingColorPicker = false } .controlSize(.small) .buttonStyle(.plain) .foregroundStyle(theme(for: colorScheme).accent) .padding(.vertical, 8) .padding(.horizontal, 14) .controlGroup(Capsule(style: .continuous), level: .primary) } .padding() .background(selectedColor.padding(-80)) .frame(width: 340) } Text(name) .font(.caption2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .frame(width: 80) .onChange(of: colorsText) { _ in selectedColor = currentColor } .onChange(of: colorScheme) { _ in selectedColor = currentColor } .onAppear { selectedColor = currentColor } } private func updateColorInTheme() { var colors = colorsText.components(separatedBy: ",").map { $0.trimmingCharacters(in: .whitespaces) } while colors.count < 5 { let defaults = colorScheme == .dark ? ThemeColors.defaultDarkColors : ThemeColors.defaultLightColors colors.append(defaults[colors.count]) } colors[colorIndex] = selectedColor.hexString colorsText = colors.joined(separator: ", ") if colorScheme == .dark { ThemeColors.saveCustomColors(dark: colorsText, light: ThemeColors.getCurrentColorsString(for: .light)) } else { ThemeColors.saveCustomColors(dark: ThemeColors.getCurrentColorsString(for: .dark), light: colorsText) } } } struct InteractiveThemeColorDemo: View { @Environment(\.colorScheme) var colorScheme @ObservedObject private var themeManager = ThemeManager.shared @Binding var colorsText: String var body: some View { HStack(spacing: 8) { ClickableColorSquare(name: "Primary BG", colorIndex: 0, colorsText: $colorsText) ClickableColorSquare(name: "Secondary BG", colorIndex: 1, colorsText: $colorsText) ClickableColorSquare(name: "Primary Text", colorIndex: 2, colorsText: $colorsText) ClickableColorSquare(name: "Secondary Text", colorIndex: 3, colorsText: $colorsText) ClickableColorSquare(name: "Accent", colorIndex: 4, colorsText: $colorsText) } .frame(maxWidth: .infinity, alignment: .center) } } struct ThemeCustomizationView: View { @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true @Environment(\.colorScheme) var colorScheme @ObservedObject private var themeManager = ThemeManager.shared @State private var colorsText: String = "" @State private var showingSaved = false @State private var showingError = false @State private var errorMessage = "" private var isDark: Bool { colorScheme == .dark } var body: some View { VStack(alignment: .leading, spacing: 20) { InteractiveThemeColorDemo(colorsText: $colorsText) VStack(alignment: .leading, spacing: 8) { Text("Color String") .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .font(.subheadline) .fontWeight(.medium) HStack { TextField("Enter exactly 5 hex colors separated by commas in the order above", text: $colorsText) .textFieldStyle(RoundedTextFieldStyle()) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .font(.system(.caption, design: .monospaced)) if showingSaved { Image(systemName: "checkmark") .foregroundStyle(.green) .font(.caption) .transition(.opacity) .padding(.leading, 5) } } } HStack { Spacer() HStack(spacing: 10) { Button("Save") { withAnimation(.easeInOut(duration: animationEnabled ? 0.2 : 0)) { saveColors() } } Divider().frame(height: 10) Button("Copy") { withAnimation(.easeInOut(duration: animationEnabled ? 0.2 : 0)) { copyToClipboard(colorsText) showSuccess() } } Divider().frame(height: 10) Button("Reset") { withAnimation(.easeInOut(duration: animationEnabled ? 0.2 : 0)) { resetColors() } } } .controlSize(.small) .buttonStyle(.plain) .foregroundStyle(theme(for: colorScheme).accent) .padding(.vertical, 8) .padding(.horizontal, 14) .controlGroup(Capsule(style: .continuous), level: .primary) Spacer() } if showingError { Text(errorMessage) .font(.caption) .foregroundStyle(.red) .transition(.opacity) } } .padding() .onAppear { loadCurrentColors() } .onChange(of: colorScheme) { _ in loadCurrentColors() } } private func loadCurrentColors() { colorsText = ThemeColors.getCurrentColorsString(for: colorScheme) clearMessages() } private func saveColors() { let colors = colorsText.components(separatedBy: ",").map { $0.trimmingCharacters(in: .whitespaces) } if colors.count != 5 { showError("Please provide exactly 5 hex colors separated by commas.") return } for (index, color) in colors.enumerated() { if !isValidHexColor(color) { showError("Color #\(index + 1) '\(color)' is not a valid hex color. Use format #rrggbb") return } } if isDark { ThemeColors.saveCustomColors(dark: colorsText, light: ThemeColors.getCurrentColorsString(for: .light)) } else { ThemeColors.saveCustomColors(dark: ThemeColors.getCurrentColorsString(for: .dark), light: colorsText) } showSuccess() } private func resetColors() { ThemeColors.resetToDefaults() loadCurrentColors() showSuccess() } private func isValidHexColor(_ hex: String) -> Bool { let trimmed = hex.trimmingCharacters(in: .whitespaces) // Check format: #rrggbb (7 characters total) guard trimmed.hasPrefix("#") && trimmed.count == 7 else { return false } let hexPart = String(trimmed.dropFirst()) return hexPart.allSatisfy { $0.isHexDigit } } private func showError(_ message: String) { errorMessage = message showingError = true showingSaved = false DispatchQueue.main.asyncAfter(deadline: .now() + 5) { clearMessages() } } private func showSuccess() { showingSaved = true showingError = false DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { clearMessages() } } private func clearMessages() { withAnimation(.easeInOut(duration: 0.3)) { showingSaved = false showingError = false } } } ================================================ FILE: Pearcleaner/Views/AppsUpdaterView/AdoptionSheetView.swift ================================================ // // AdoptionSheetView.swift // Pearcleaner // // Created by Alin Lupascu on 11/18/25. // import SwiftUI enum AdoptionContext { case updaterView case filesView } struct AdoptionSheetView: View { let appInfo: AppInfo let context: AdoptionContext @EnvironmentObject var brewManager: HomebrewManager @Environment(\.colorScheme) var colorScheme @Binding var isPresented: Bool @State private var matchingCasks: [AdoptableCask] = [] @State private var selectedCaskToken: String? = nil @State private var manualEntry: String = "" @State private var manualEntryValidation: AdoptableCask? = nil @State private var isAdopting: Bool = false @State private var adoptionError: String? = nil @State private var isSearching: Bool = true var body: some View { StandardSheetView( title: "Adopt \(appInfo.appName) with Homebrew", width: 700, height: 600, onClose: { isPresented = false }, content: { VStack(alignment: .leading, spacing: 20) { // Description Text("Select a Homebrew cask to manage this app. Homebrew will adopt the existing installation without moving or duplicating files.") .font(.body) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .fixedSize(horizontal: false, vertical: true) // Matching casks section if isSearching { HStack { Spacer() VStack(spacing: 12) { ProgressView() .scaleEffect(1.2) Text("Searching for matching casks...") .font(.body) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } Spacer() } .frame(maxHeight: .infinity) } else { CaskAdoptionContentView( matchingCasks: $matchingCasks, selectedCaskToken: $selectedCaskToken, manualEntry: $manualEntry, manualEntryValidation: $manualEntryValidation, adoptionError: $adoptionError, onManualEntryChange: validateManualEntry, limitCaskListHeight: true ) } } }, actionButtons: { HStack(spacing: 12) { Button("Cancel") { isPresented = false } .buttonStyle(.plain) .disabled(isAdopting) Button(isAdopting ? "Adopting..." : "Adopt") { performAdoption() } .buttonStyle(.borderedProminent) .disabled(isAdopting || !canAdopt) } } ) .onAppear { searchForMatchingCasks() } } // MARK: - Computed Properties private var canAdopt: Bool { if isSearching { return false } if isAdopting { return false } // Either a cask is selected from the list, or manual entry is valid if let selected = selectedCaskToken, !selected.isEmpty { return true } if manualEntryValidation != nil, !manualEntry.isEmpty { return true } return false } private var selectedCask: AdoptableCask? { if let token = selectedCaskToken { return matchingCasks.first(where: { $0.token == token }) } return manualEntryValidation } // MARK: - Methods private func searchForMatchingCasks() { isSearching = true Task { // Small delay to show loading state try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds let matches = findMatchingCasks(for: appInfo, from: brewManager.allAvailableCasks) await MainActor.run { matchingCasks = matches isSearching = false // Auto-select first compatible cask if there's only one if matches.count == 1, matches[0].isVersionCompatible { selectedCaskToken = matches[0].token } } } } private func validateManualEntry(_ token: String) { guard !token.isEmpty, token.count >= 2 else { manualEntryValidation = nil return } // Validate against loaded casks let validated = validateManualCaskEntry(token, for: appInfo, from: brewManager.allAvailableCasks) if validated != nil { manualEntryValidation = validated selectedCaskToken = nil // Clear list selection when manual entry is valid } else { manualEntryValidation = nil } } private func performAdoption() { guard let cask = selectedCask else { return } isAdopting = true adoptionError = nil Task { do { try await HomebrewController.shared.adoptCask(token: cask.token) // Success - reload installed packages await brewManager.loadInstalledPackages() // Clear cask cache to pick up new adoption invalidateCaskLookupCache() // Reload apps to get updated cask metadata (non-streaming for full AppInfo) let folderPaths = await MainActor.run { FolderSettingsManager.shared.folderPaths } await loadAppsAsync(folderPaths: folderPaths, useStreaming: false) // Trigger Updater refresh to recategorize and check for updates await MainActor.run { // Only update AppState.appInfo when adopting from FilesView if context == .filesView { // Create updated AppInfo copy with new cask token let updatedAppInfo = AppInfo( id: appInfo.id, path: appInfo.path, bundleIdentifier: appInfo.bundleIdentifier, appName: appInfo.appName, appVersion: appInfo.appVersion, appBuildNumber: appInfo.appBuildNumber, appIcon: appInfo.appIcon, webApp: appInfo.webApp, wrapped: appInfo.wrapped, system: appInfo.system, arch: appInfo.arch, cask: cask.token, steam: appInfo.steam, hasSparkle: appInfo.hasSparkle, isAppStore: appInfo.isAppStore, adamID: appInfo.adamID, autoUpdates: cask.autoUpdates, bundleSize: appInfo.bundleSize, lipoSavings: appInfo.lipoSavings, fileSize: appInfo.fileSize, fileIcon: appInfo.fileIcon, creationDate: appInfo.creationDate, contentChangeDate: appInfo.contentChangeDate, lastUsedDate: appInfo.lastUsedDate, dateAdded: appInfo.dateAdded, entitlements: appInfo.entitlements, teamIdentifier: appInfo.teamIdentifier ) // Update AppState.appInfo immediately for UI feedback AppState.shared.appInfo = updatedAppInfo // Also update in sortedApps array if let index = AppState.shared.sortedApps.firstIndex(where: { $0.path == appInfo.path }) { AppState.shared.sortedApps[index] = updatedAppInfo } } // Always trigger Updater refresh (for both contexts) NotificationCenter.default.post( name: NSNotification.Name("UpdaterViewShouldRefresh"), object: nil ) isPresented = false } } catch { await MainActor.run { isAdopting = false adoptionError = "Failed to adopt: \(error.localizedDescription)" } } } } } // MARK: - Cask Row View struct CaskRowView: View { let cask: AdoptableCask let isSelected: Bool let onSelect: () -> Void @Environment(\.colorScheme) var colorScheme var body: some View { Button(action: onSelect) { HStack(alignment: .top, spacing: 12) { // Radio button indicator Image(systemName: isSelected ? "checkmark.circle.fill" : "circle") .font(.system(size: 16)) .foregroundStyle(isSelected ? ThemeColors.shared(for: colorScheme).accent : ThemeColors.shared(for: colorScheme).secondaryText) VStack(alignment: .leading, spacing: 6) { // Cask name and token HStack(spacing: 8) { Text(cask.displayName) .font(.headline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) if cask.displayName != cask.token { Text(verbatim: "(\(cask.token))") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } // Version info HStack(spacing: 4) { Text("Version:") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text(cask.version) .font(.caption) .foregroundStyle(cask.isVersionCompatible ? .green : .orange) if cask.autoUpdates { Text("• Auto-updates") .font(.caption) .foregroundStyle(.green) } // else if !cask.isVersionCompatible { // Text("• Version mismatch") // .font(.caption) // .foregroundStyle(.orange) // } } // Description if let description = cask.description { Text(description) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .lineLimit(2) } // Homepage link if let homepage = cask.homepage, let url = URL(string: homepage) { Link(destination: url) { HStack(spacing: 4) { Image(systemName: "link") .font(.caption2) Text(homepage) .font(.caption2) .lineLimit(1) } .foregroundStyle(.blue) } .buttonStyle(.plain) } // Match confidence indicator let confidence = matchConfidence(cask.matchScore) HStack(spacing: 2) { Text(confidence.label) .font(.caption) .foregroundStyle(confidence.color) Text("match confidence") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } Spacer() } .padding(12) .background( RoundedRectangle(cornerRadius: 8) .fill(isSelected ? ThemeColors.shared(for: colorScheme).accent.opacity(0.1) : ThemeColors.shared(for: colorScheme).secondaryBG ) ) .overlay( RoundedRectangle(cornerRadius: 8) .strokeBorder(isSelected ? ThemeColors.shared(for: colorScheme).accent : Color.clear, lineWidth: 2 ) ) } .buttonStyle(.plain) } private func matchConfidence(_ score: Int) -> (label: String, color: Color) { if score >= 80 { return ("High", .green) } if score >= 30 { return ("Medium", .orange) } return ("Low", .red) } } ================================================ FILE: Pearcleaner/Views/AppsUpdaterView/AppsUpdaterView.swift ================================================ // // AppsUpdaterView.swift // Pearcleaner // // Created by Alin Lupascu on 10/13/25. // import SwiftUI import AlinFoundation struct AppsUpdaterView: View { @EnvironmentObject var updateManager: UpdateManager @EnvironmentObject var appState: AppState @EnvironmentObject var updater: Updater @Environment(\.colorScheme) var colorScheme @ObservedObject private var consoleManager = GlobalConsoleManager.shared @State private var searchText = "" @State private var hiddenSidebar: Bool = false @State private var selectedApp: UpdateableApp? = nil @AppStorage("settings.interface.scrollIndicators") private var scrollIndicators: Bool = false @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true @AppStorage("settings.updater.sources") private var sourcesData: Data = UpdaterSourcesSettings.defaultEncoded() @AppStorage("settings.updater.display") private var displayData: Data = UpdaterDisplaySettings.defaultEncoded() @AppStorage("settings.interface.startupView") private var startupView: Int = CurrentPage.applications.rawValue // Computed properties for convenient access private var sources: UpdaterSourcesSettings { get { UpdaterSourcesSettings.decode(from: sourcesData) } set { sourcesData = newValue.encode() } } private var display: UpdaterDisplaySettings { get { UpdaterDisplaySettings.decode(from: displayData) } set { displayData = newValue.encode() } } // Collect all apps across all sources (exclude unsupported and current apps - they can't/don't need updates) private var allApps: [UpdateableApp] { updateManager.updatesBySource.values.flatMap { $0 }.filter { $0.source != .unsupported && $0.source != .current } } // Count selected apps across all sources private var selectedAppsCount: Int { allApps.filter { $0.isSelectedForUpdate }.count } // Sidebar categories private var sidebarCategories: [(String, (UpdateableApp) -> Bool, Bool, Bool)] { var cats: [(String, (UpdateableApp) -> Bool, Bool, Bool)] = [] if sources.appStore.enabled { cats.append(("App Store", { $0.source == .appStore }, true, updateManager.scanningSources.contains(.appStore))) } if sources.homebrew.enabled { cats.append(("Homebrew", { $0.source == .homebrew }, true, updateManager.scanningSources.contains(.homebrew))) } if sources.sparkle.enabled { cats.append(("Sparkle", { $0.source == .sparkle }, true, updateManager.scanningSources.contains(.sparkle))) } // Show Current category if enabled if display.showCurrent { cats.append(("Current", { $0.source == .current }, false, false)) } // Show Unsupported if enabled if display.showUnsupported { cats.append(("Unsupported", { $0.source == .unsupported }, false, false)) } return cats } // All updateable apps for sidebar private var allUpdateableApps: [UpdateableApp] { updateManager.updatesBySource.values.flatMap { $0 } } var body: some View { ZStack { SidebarDetailLayout { GenericSidebarListView( items: allUpdateableApps, categories: sidebarCategories, searchText: $searchText, emptyMessage: "No apps to update", noResultsMessage: "No matching apps", isLoading: updateManager.isScanning, loadingMessage: "Loading.." ) { app in UpdateRowViewSidebar( app: app, isSelected: selectedApp?.id == app.id, onTap: { if selectedApp?.id == app.id { // Deselect if tapping the same app selectedApp = nil } else { // Select new app selectedApp = app } } ) } } detail: { Group { if let app = selectedApp { UpdateDetailView(appId: app.id) } else { VStack { Spacer() Text("Select an app to view details") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .font(.title2) Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) } } } .onChange(of: allUpdateableApps) { newApps in // Auto-clear selectedApp if it no longer exists in the updates list if let selected = selectedApp, !newApps.contains(where: { $0.id == selected.id }) { selectedApp = nil } } .opacity(hiddenSidebar ? 0.5 : 1) UpdaterDetailsSidebar( hiddenSidebar: $hiddenSidebar, sources: Binding( get: { self.sources }, set: { newValue in self.sourcesData = newValue.encode() } ), display: Binding( get: { self.display }, set: { newValue in self.displayData = newValue.encode() } ) ) } .animation(animationEnabled ? .spring(response: 0.35, dampingFraction: 0.8) : .none, value: hiddenSidebar) .transition(.opacity) .frame(maxWidth: .infinity, maxHeight: .infinity) .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("UpdaterViewShouldRefresh"))) { _ in Task { let task = Task { await updateManager.scanIfNeeded(forceReload: true) } updateManager.currentScanTask = task await task.value } } .toolbar { // TahoeToolbarItem(placement: .navigation) { // HStack { // VStack(alignment: .leading) { // Text("Updater") // .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) // .font(.title2) // .fontWeight(.bold) // Text("Check for app updates from App Store, Homebrew and Sparkle") // .font(.callout) // .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) // } // BetaBadge() // } // } ToolbarItem { Spacer() } TahoeToolbarItem(isGroup: true) { Button { withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) { consoleManager.showConsole.toggle() } } label: { Label("Console", systemImage: consoleManager.showConsole ? "terminal.fill" : "terminal") } .help("Toggle console output") Button { selectAllApps() updateSelectedApps() } label: { if updateManager.isUpdatingAll { // Show circular progress with count during batch update CircularProgressView( progress: updateManager.updateAllProgress, size: 20, lineWidth: 3, badgeText: "\(updateManager.completedAppsCount)/\(updateManager.totalAppsToUpdate)" ) } else { Label("Update All", systemImage: "arrow.down.circle") .badge(updateManager.totalUpdateCount) } } .help(updateManager.isUpdatingAll ? "Updating apps..." : "Update all available apps") .disabled(allUpdateableApps.isEmpty || !updateManager.scanningSources.isEmpty || updateManager.isUpdatingAll) if updateManager.isScanning { // Show stop button during scan Button { updateManager.cancelScan() } label: { Label("Stop", systemImage: "stop.circle") } .help("Stop checking for updates") } else { // Show refresh button when not scanning Button { GlobalConsoleManager.shared.appendOutput("Refreshing app updates...\n", source: CurrentPage.updater.title) Task { let task = Task { await updateManager.scanIfNeeded(forceReload: true) } updateManager.currentScanTask = task await task.value GlobalConsoleManager.shared.appendOutput("✓ Completed update scan\n", source: CurrentPage.updater.title) } } label: { Label("Refresh", systemImage: "arrow.counterclockwise") } .help("Scan for app updates") } Button { hiddenSidebar.toggle() } label: { Label("Hidden", systemImage: "sidebar.trailing") } .help("Show hidden updates") } } .task { // Skip scan if background scan already completed guard updateManager.lastScanDate == nil else { GlobalConsoleManager.shared.appendOutput("Using cached updates from background scan\n", source: CurrentPage.updater.title) return } GlobalConsoleManager.shared.appendOutput("Loading app updates...\n", source: CurrentPage.updater.title) let task = Task { await updateManager.scanIfNeeded() } updateManager.currentScanTask = task await task.value GlobalConsoleManager.shared.appendOutput("✓ Loaded app updates\n", source: CurrentPage.updater.title) } .onDisappear { UpdaterDebugLogger.shared.clearLogs() } } private func selectAllApps() { // Select all apps across all sources (skip unsupported - they can't be updated) for (source, apps) in updateManager.updatesBySource { guard source != .unsupported || source != .current else { continue } var updatedApps = apps for index in updatedApps.indices { updatedApps[index].isSelectedForUpdate = true } updateManager.updatesBySource[source] = updatedApps } } // private func deselectAllApps() { // // Deselect all apps across all sources (skip unsupported - they can't be updated) // for (source, apps) in updateManager.updatesBySource { // guard source != .unsupported else { continue } // var updatedApps = apps // for index in updatedApps.indices { // updatedApps[index].isSelectedForUpdate = false // } // updateManager.updatesBySource[source] = updatedApps // } // } private func updateSelectedApps() { GlobalConsoleManager.shared.appendOutput("Starting update of \(selectedAppsCount) selected app(s)...\n", source: CurrentPage.updater.title) Task { await updateManager.updateSelectedApps() GlobalConsoleManager.shared.appendOutput("✓ Completed update operation\n", source: CurrentPage.updater.title) } } } ================================================ FILE: Pearcleaner/Views/AppsUpdaterView/CaskAdoptionContentView.swift ================================================ // // CaskAdoptionContentView.swift // Pearcleaner // // Reusable adoption UI content shared between AdoptionSheetView and UpdateDetailView // import SwiftUI struct CaskAdoptionContentView: View { // Required bindings @Binding var matchingCasks: [AdoptableCask] @Binding var selectedCaskToken: String? @Binding var manualEntry: String @Binding var manualEntryValidation: AdoptableCask? @Binding var adoptionError: String? // Callbacks let onManualEntryChange: (String) -> Void // Optional customization let limitCaskListHeight: Bool let showManualEntry: Bool @Environment(\.colorScheme) var colorScheme init( matchingCasks: Binding<[AdoptableCask]>, selectedCaskToken: Binding, manualEntry: Binding, manualEntryValidation: Binding, adoptionError: Binding, onManualEntryChange: @escaping (String) -> Void, limitCaskListHeight: Bool, showManualEntry: Bool = true ) { self._matchingCasks = matchingCasks self._selectedCaskToken = selectedCaskToken self._manualEntry = manualEntry self._manualEntryValidation = manualEntryValidation self._adoptionError = adoptionError self.onManualEntryChange = onManualEntryChange self.limitCaskListHeight = limitCaskListHeight self.showManualEntry = showManualEntry } var body: some View { VStack(alignment: .leading, spacing: 20) { // Matching casks section VStack(alignment: .leading, spacing: 12) { Text("Casks") .font(.headline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) if matchingCasks.isEmpty { Text("No matching casks found. Try manual entry below.") .font(.body) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .italic() .padding(.vertical, 8) .frame(maxWidth: .infinity, alignment: .leading) } else { ScrollView { VStack(spacing: 8) { ForEach(matchingCasks) { cask in CaskRowView( cask: cask, isSelected: selectedCaskToken == cask.token, onSelect: { selectedCaskToken = cask.token manualEntry = "" // Clear manual entry when selecting from list manualEntryValidation = nil } ) } } } .frame(maxHeight: limitCaskListHeight ? 250 : nil) } } // Manual entry section (optional) if showManualEntry { VStack(alignment: .leading, spacing: 8) { Text("Manual Entry") .font(.headline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Text("If the correct cask isn't listed above, enter the cask token manually:") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) HStack(spacing: 8) { TextField("e.g., firefox", text: $manualEntry) .textFieldStyle(.roundedBorder) .onChange(of: manualEntry) { newValue in onManualEntryChange(newValue) } if !manualEntry.isEmpty { if let validation = manualEntryValidation { Image(systemName: "checkmark.circle.fill") .foregroundStyle(.green) .help("Valid cask: \(validation.displayName)") } else if manualEntry.count >= 2 { Image(systemName: "xmark.circle.fill") .foregroundStyle(.red) .help("Cask not found") } } } } } // Error message if let error = adoptionError { HStack(spacing: 8) { Image(systemName: "exclamationmark.triangle.fill") .foregroundStyle(.red) Text(error) .font(.caption) .foregroundStyle(.red) } .padding(.vertical, 8) } } } } ================================================ FILE: Pearcleaner/Views/AppsUpdaterView/ExpandableActionButton.swift ================================================ // // ExpandableActionButton.swift // Pearcleaner // // Created by Alin Lupascu on 11/25/25. // import SwiftUI import AlinFoundation struct ExpandableActionButton: View { let primaryAction: ActionButtonItem let secondaryActions: [ActionButtonItem] @Environment(\.colorScheme) var colorScheme var body: some View { HStack(spacing: 0) { // Primary action button Button(action: primaryAction.action) { Text(primaryAction.title) .foregroundStyle(primaryAction.foregroundColor) .padding(.leading, 4) .padding(.horizontal, 10) .padding(.vertical, 4) .contentShape(Rectangle()) } .buttonStyle(.plain) .disabled(primaryAction.isDisabled) if !secondaryActions.isEmpty { // Divider Divider() .frame(height: 20) // .padding(.horizontal, 4) // Chevron button that shows NSMenu MenuButton( items: secondaryActions, colorScheme: colorScheme ) .frame(width: 20, height: 20) .padding(.horizontal, 5) .padding(.vertical, 2) } } .background( LinearGradient( gradient: Gradient(colors: [ primaryAction.backgroundColor.adjust(brightness: colorScheme == .dark ? 0.05 : 0), primaryAction.backgroundColor.adjust(brightness: colorScheme == .dark ? -0.05 : 0) ]), startPoint: .top, endPoint: .bottom ) ) .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) .overlay( RoundedRectangle(cornerRadius: 20, style: .continuous) .stroke( LinearGradient( gradient: Gradient(stops: [ .init(color: Color.white.opacity(0.4), location: 0.0), .init(color: Color.white.opacity(0.2), location: 0.5), .init(color: Color.black.opacity(0.3), location: 1.0) ]), startPoint: .top, endPoint: .bottom ), lineWidth: 1 ) ) .opacity(primaryAction.isDisabled ? 0.5 : 1.0) } // MARK: - NSViewRepresentable for Menu Button private struct MenuButton: NSViewRepresentable { let items: [ActionButtonItem] let colorScheme: ColorScheme func makeNSView(context: Context) -> NSView { let button = NSButton() button.title = "" // Create smaller chevron image let config = NSImage.SymbolConfiguration(pointSize: 10, weight: .regular) button.image = NSImage(systemSymbolName: "chevron.down", accessibilityDescription: nil)?.withSymbolConfiguration(config) button.isBordered = false button.bezelStyle = .regularSquare button.target = context.coordinator button.action = #selector(Coordinator.showMenu(_:)) // Use ThemeColors primary text color button.contentTintColor = NSColor(ThemeColors.shared(for: colorScheme).primaryText) // Make the entire button area clickable button.imagePosition = .imageOnly // Set size constraints to match SwiftUI padding button.translatesAutoresizingMaskIntoConstraints = false NSLayoutConstraint.activate([ button.widthAnchor.constraint(equalToConstant: 24), button.heightAnchor.constraint(equalToConstant: 24) ]) return button } func updateNSView(_ button: NSView, context: Context) { guard let button = button as? NSButton else { return } button.contentTintColor = NSColor(ThemeColors.shared(for: colorScheme).primaryText) } func makeCoordinator() -> Coordinator { Coordinator(items: items) } class Coordinator: NSObject { let items: [ActionButtonItem] init(items: [ActionButtonItem]) { self.items = items } @objc func showMenu(_ sender: NSButton) { let menu = NSMenu() for item in items { let menuItem = NSMenuItem( title: NSLocalizedString(item.title, comment: ""), action: #selector(handleMenuAction(_:)), keyEquivalent: "" ) // Apply custom color via NSAttributedString menuItem.attributedTitle = NSAttributedString( string: NSLocalizedString(item.title, comment: ""), attributes: [ .foregroundColor: NSColor(item.foregroundColor), .font: NSFont.systemFont(ofSize: NSFont.systemFontSize) ] ) menuItem.target = self menuItem.representedObject = item.action menuItem.isEnabled = !item.isDisabled menu.addItem(menuItem) } // Show menu below the button let location = NSPoint(x: 0, y: sender.bounds.height + 4) menu.popUp(positioning: nil, at: location, in: sender) } @objc func handleMenuAction(_ sender: NSMenuItem) { if let action = sender.representedObject as? () -> Void { action() } } } } } struct ActionButtonItem { let title: String let foregroundColor: Color let backgroundColor: Color let isDisabled: Bool let action: () -> Void init( title: String, foregroundColor: Color, backgroundColor: Color, isDisabled: Bool = false, action: @escaping () -> Void ) { self.title = title self.foregroundColor = foregroundColor self.backgroundColor = backgroundColor self.isDisabled = isDisabled self.action = action } } ================================================ FILE: Pearcleaner/Views/AppsUpdaterView/UpdateDetailView.swift ================================================ // // UpdateDetailView.swift // Pearcleaner // // Created by Alin Lupascu on 11/19/25. // import SwiftUI import AlinFoundation struct UpdateDetailView: View { let appId: UUID @EnvironmentObject var updateManager: UpdateManager @EnvironmentObject var brewManager: HomebrewManager @EnvironmentObject var appState: AppState @Environment(\.colorScheme) var colorScheme @AppStorage("settings.interface.scrollIndicators") private var scrollIndicators: Bool = false @State private var showAdoptionSheet: Bool = false @State private var isLoadingCasks: Bool = false // Inline adoption state for unsupported apps @State private var matchingCasks: [AdoptableCask] = [] @State private var selectedCaskToken: String? = nil @State private var manualEntry: String = "" @State private var manualEntryValidation: AdoptableCask? = nil @State private var isAdopting: Bool = false @State private var adoptionError: String? = nil @State private var isSearchingCasks: Bool = false @State private var showMASWarning = false // Look up live app data from updateManager - this makes the view reactive to status changes private var app: UpdateableApp? { updateManager.updatesBySource.values .flatMap { $0 } .first { $0.id == appId } } private var sourceColor: Color { guard let app = app else { return .gray } switch app.source { case .homebrew: return .green case .appStore: return .purple case .sparkle: return .orange case .unsupported: return .gray case .current: return .green } } private var isNonPrimaryRegion: Bool { guard let app = app, app.source == .appStore, let foundRegion = app.foundInRegion else { return false } let primaryRegion = Locale.autoupdatingCurrent.region?.identifier ?? "US" return foundRegion != primaryRegion } var body: some View { Group { if let app = app { contentView(for: app) } else { VStack { Spacer() Text("App not found") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .font(.title2) Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) } } .confirmationDialog( "App Store App Detected", isPresented: $showMASWarning, titleVisibility: .visible ) { if let app = app { Button("Update Anyway", role: .destructive) { Task { await updateManager.updateApp(app) } } if let availableVersion = app.availableVersion { let displayVersion = app.source == .homebrew ? availableVersion.stripBrewRevisionSuffix() : availableVersion Button("Skip \(displayVersion)") { updateManager.hideApp(app, skipVersion: app.availableVersion) } } else { Button("Skip This Version") { updateManager.hideApp(app, skipVersion: app.availableVersion) } } Button("Cancel", role: .cancel) { } } } message: { if let app = app { let sourceName = app.source == .sparkle ? "Sparkle" : app.source == .homebrew ? "Homebrew" : "this source" Text("This app was installed from the App Store. Updating it via \(sourceName) will:\n• Break the App Store receipt and licensing\n• Remove the app from App Store tracking\n• Prevent future App Store updates\n\nProceed with caution.") } } } @ViewBuilder private func contentView(for app: UpdateableApp) -> some View { VStack(alignment: .leading, spacing: 20) { // Header: App icon, name, version, action buttons VStack(alignment: .leading, spacing: 16) { HStack(spacing: 16) { if let appIcon = app.appInfo.appIcon { Image(nsImage: appIcon) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 80, height: 80) .clipShape(RoundedRectangle(cornerRadius: 16)) } VStack(alignment: .leading, spacing: 8) { HStack(spacing: 8) { Text(app.appInfo.appName) .font(.title) .fontWeight(.bold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) // Pre-release indicator (Sparkle only) if app.source == .sparkle && app.isPreRelease { if #available(macOS 14.0, *) { Image(systemName: "flask.fill") .font(.title3) .foregroundStyle(.green) .help("Pre-release") } else { Image(systemName: "testtube.2") .font(.title3) .foregroundStyle(.green) .help("Pre-release") } } // App Store button (App Store only) if app.source == .appStore, let appStoreURL = app.appStoreURL { Button { openInAppStore(urlString: appStoreURL) } label: { Image(systemName: ifOSBelow(macOS: 14) ? "cart.fill" : "storefront.fill") .font(.title3) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) } .buttonStyle(.plain) .help("View in App Store") .id(app.id) } // Unsupported indicator if app.source == .unsupported { Image(systemName: "exclamationmark.triangle.fill") .font(.title3) .foregroundStyle(.orange) .help("Unsupported") } Spacer() HStack(spacing: 8) { // Status view (leading edge of button) statusView(for: app) // Action button (right edge) buildExpandableActionButton(for: app) .id(app.id) } } // Version info with build numbers buildVersionText(for: app, colorScheme: colorScheme) .font(.title3) .help(buildNumberTooltip(for: app) ?? "") // Non-primary region warning if isNonPrimaryRegion, let region = app.foundInRegion { Text("Found in \(region) App Store. Open in App Store to update.") .font(.caption) .foregroundStyle(.orange) } } } } Divider() // Unsupported apps: Show inline adoption view if app.source == .unsupported { unsupportedContentView(for: app) .id(app.id) } // Current apps: Show simple "up to date" message else if app.source == .current { VStack { Spacer() Image(systemName: "checkmark.circle.fill") .font(.system(size: 48)) .foregroundStyle(.green) Text("App is up to date") .font(.title2) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .padding(.top, 8) Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) .id(app.id) } // All other apps: Show normal info and release notes else { // Release details (only for apps with updates) VStack(alignment: .leading, spacing: 16) { // Release description (HTML formatted) - scrollable section // Priority 1: Fetched external notes, Priority 2: Inline description if let preprocessed = processedReleaseNotes(for: app), !preprocessed.isEmpty { ScrollView { // Try HTML parsing first if let htmlAttributedString = try? NSAttributedString( data: Data(preprocessed.utf8), options: [.documentType: NSAttributedString.DocumentType.html, .characterEncoding: String.Encoding.utf8.rawValue], documentAttributes: nil ) { // Check if HTML parsing collapsed everything to one line let lineCount = htmlAttributedString.string.split(separator: "\n").count if lineCount == 1, let plainAttributedString = try? NSAttributedString( data: Data(preprocessed.utf8), options: [.documentType: NSAttributedString.DocumentType.plain, .characterEncoding: String.Encoding.utf8.rawValue], documentAttributes: nil ) { // Fallback to plain text parsing to preserve formatting let standardizedString = standardizeFont(in: plainAttributedString) Text(standardizedString) .frame(maxWidth: .infinity, alignment: .leading) .textSelection(.enabled) } else { // HTML parsing preserved structure, use it let standardizedString = standardizeFont(in: htmlAttributedString) Text(standardizedString) .frame(maxWidth: .infinity, alignment: .leading) .textSelection(.enabled) } } else { // HTML parsing failed entirely, show raw text (use original, untrimmed) let originalDescription = (app.fetchedReleaseNotes ?? app.releaseDescription) ?? "" Text(originalDescription) .font(.body) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .frame(maxWidth: .infinity, alignment: .leading) .textSelection(.enabled) } } .scrollIndicators(scrollIndicators ? .visible : .hidden) .frame(maxHeight: .infinity) } else { // No release notes found - show message VStack { Spacer() Text("No release notes were found") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .font(.callout) Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) } } .padding(.horizontal, 5) .id(app.id) // Metadata row at bottom Divider() metadataRow(for: app) .id(app.id) } Spacer() } .padding(.horizontal, 15) .frame(maxWidth: .infinity, maxHeight: .infinity) .sheet(isPresented: $showAdoptionSheet) { AdoptionSheetView( appInfo: app.appInfo, context: .updaterView, isPresented: $showAdoptionSheet ) } .onAppear { loadCasksForAdoption() } .onChange(of: appId) { newAppId in // Clear cask search state when switching apps matchingCasks = [] selectedCaskToken = nil manualEntry = "" manualEntryValidation = nil isAdopting = false adoptionError = nil isSearchingCasks = false // Re-trigger cask search for new unsupported app // Use Task to allow SwiftUI to update with new app first Task { // Look up the new app directly from updateManager if let newApp = updateManager.updatesBySource.values .flatMap({ $0 }) .first(where: { $0.id == newAppId }), newApp.source == .unsupported { await MainActor.run { searchForMatchingCasks(for: newApp) } } } } } @ViewBuilder private func metadataRow(for app: UpdateableApp) -> some View { HStack(spacing: 0) { // Released date HStack(spacing: 0) { Spacer() if let date = app.releaseDate { Text(formatDate(date)) .font(.subheadline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } else { Text("No Release Date") .font(.subheadline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } Spacer() } // Source HStack(spacing: 0) { Spacer() HStack(spacing: 4) { Text(app.source.rawValue) .font(.subheadline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) if app.isIOSApp { Text(verbatim: "(iOS)") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } Spacer() } // Changelog HStack(spacing: 0) { Spacer() if let link = app.releaseNotesLink, let url = URL(string: link) { Link(destination: url) { Text("View Changelog") .font(.subheadline) .foregroundStyle(.blue) } .buttonStyle(.plain) } else { Text("No Changelog") .font(.subheadline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } Spacer() } .id(app.id) } .padding(.vertical, 5) } @ViewBuilder private func statusView(for app: UpdateableApp) -> some View { switch app.status { case .checking, .downloading, .extracting, .installing, .verifying: HStack(spacing: 6) { Text(statusText(for: app.status)) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) CircularProgressView( progress: app.progress, size: 20, lineWidth: 5 ) } case .completed: Text(statusText(for: app.status)) .font(.caption) .foregroundStyle(.green) case .failed: Text(statusText(for: app.status)) .font(.caption) .foregroundStyle(.red) case .idle: EmptyView() } } @ViewBuilder private func actionButton(for app: UpdateableApp) -> some View { Button { if app.isIOSApp, let appStoreURL = app.appStoreURL { openInAppStore(urlString: appStoreURL) } else if isNonPrimaryRegion, let appStoreURL = app.appStoreURL { openInAppStore(urlString: appStoreURL) } else { Task { await updateManager.updateApp(app) } } } label: { Text(app.isIOSApp || isNonPrimaryRegion ? "Update in App Store" : "Update") } .buttonStyle(.plain) .padding(.horizontal, 16) .padding(.vertical, 8) .background(Color.green) .foregroundStyle(.white) .clipShape(Capsule()) .disabled(app.status != .idle) .opacity(app.status == .idle ? 1.0 : 0.5) } private func buildExpandableActionButton(for app: UpdateableApp) -> ExpandableActionButton { // Current apps: Hide (primary) + Adopt (secondary, if applicable) if app.source == .current { let primaryAction = ActionButtonItem( title: "Hide \(app.appInfo.appName)", foregroundColor: .red, backgroundColor: ThemeColors.shared(for: colorScheme).secondaryBG, action: { updateManager.hideApp(app, skipVersion: nil) } ) var secondaryActions: [ActionButtonItem] = [] // Add Adopt button for non-Homebrew current apps if app.appInfo.cask == nil { secondaryActions.append(ActionButtonItem( title: "Adopt", foregroundColor: ThemeColors.shared(for: colorScheme).accent, backgroundColor: ThemeColors.shared(for: colorScheme).secondaryBG, isDisabled: isLoadingCasks, action: { if brewManager.allAvailableCasks.isEmpty { isLoadingCasks = true Task { await brewManager.loadAvailablePackages(appState: appState) await MainActor.run { isLoadingCasks = false showAdoptionSheet = true } } } else { showAdoptionSheet = true } } )) } return ExpandableActionButton( primaryAction: primaryAction, secondaryActions: secondaryActions ) } // Unsupported apps: Hide only (no dropdown) else if app.source == .unsupported { let primaryAction = ActionButtonItem( title: "Hide", foregroundColor: .red, backgroundColor: ThemeColors.shared(for: colorScheme).primaryBG, action: { updateManager.hideApp(app, skipVersion: nil) } ) return ExpandableActionButton( primaryAction: primaryAction, secondaryActions: [] ) } // Apps with updates: Update (primary) + Skip + Hide + Adopt (secondary) else { let primaryAction = ActionButtonItem( title: app.isIOSApp || isNonPrimaryRegion ? "Update in App Store" : "Update", foregroundColor: .white, backgroundColor: .green, isDisabled: app.status != .idle, action: { if app.isIOSApp, let appStoreURL = app.appStoreURL { openInAppStore(urlString: appStoreURL) } else if isNonPrimaryRegion, let appStoreURL = app.appStoreURL { openInAppStore(urlString: appStoreURL) } else if app.appInfo.isAppStore && app.source != .appStore { // Warn for ANY non-App Store update (Sparkle, Homebrew, etc.) showMASWarning = true } else { Task { await updateManager.updateApp(app) } } } ) var secondaryActions: [ActionButtonItem] = [] // Skip button if let availableVersion = app.availableVersion { let displayVersion = app.source == .homebrew ? availableVersion.stripBrewRevisionSuffix() : availableVersion secondaryActions.append(ActionButtonItem( title: "Skip \(displayVersion)", foregroundColor: .orange, backgroundColor: ThemeColors.shared(for: colorScheme).secondaryBG, action: { updateManager.hideApp(app, skipVersion: availableVersion) } )) } // Hide button secondaryActions.append(ActionButtonItem( title: "Hide", foregroundColor: .red, backgroundColor: ThemeColors.shared(for: colorScheme).secondaryBG, action: { updateManager.hideApp(app, skipVersion: nil) } )) // Adopt button for non-Homebrew apps if app.source != .homebrew && app.appInfo.cask == nil { secondaryActions.append(ActionButtonItem( title: "Adopt", foregroundColor: ThemeColors.shared(for: colorScheme).accent, backgroundColor: ThemeColors.shared(for: colorScheme).secondaryBG, isDisabled: isLoadingCasks, action: { if brewManager.allAvailableCasks.isEmpty { isLoadingCasks = true Task { await brewManager.loadAvailablePackages(appState: appState) await MainActor.run { isLoadingCasks = false showAdoptionSheet = true } } } else { showAdoptionSheet = true } } )) } return ExpandableActionButton( primaryAction: primaryAction, secondaryActions: secondaryActions ) } } private func statusText(for status: UpdateStatus) -> String { switch status { case .idle: return "" case .checking: return "Checking..." case .downloading: return "Downloading..." case .extracting: return "Extracting..." case .installing: return "Installing..." case .verifying: return "Verifying installation..." case .completed: return "Completed" case .failed(let message): return "Failed: \(message)" } } private func buildVersionText(for app: UpdateableApp, colorScheme: ColorScheme) -> Text { // Current apps: Show only installed version (no arrow) if app.source == .current { if app.source == .sparkle, let installedBuild = app.appInfo.appBuildNumber { let displayBuild = installedBuild.count > 5 ? String(installedBuild.prefix(5)) + "..." : installedBuild return Text(verbatim: "\(app.appInfo.appVersion) (\(displayBuild))") } return Text(verbatim: app.appInfo.appVersion) } guard let availableVersion = app.availableVersion else { if app.source == .sparkle, let installedBuild = app.appInfo.appBuildNumber { let displayBuild = installedBuild.count > 5 ? String(installedBuild.prefix(5)) + "..." : installedBuild return Text(verbatim: "\(app.appInfo.appVersion) (\(displayBuild))") } return Text(verbatim: app.appInfo.appVersion) } let displayInstalledVersion = app.source == .homebrew ? app.appInfo.appVersion.stripBrewRevisionSuffix() : app.appInfo.appVersion let displayAvailableVersion = app.source == .homebrew ? availableVersion.stripBrewRevisionSuffix() : availableVersion // Full version display with build numbers for detail view if app.source == .sparkle { let installedBuild = app.appInfo.appBuildNumber let availableBuild = app.availableBuildNumber // Truncate build numbers to 6 characters let displayInstalledBuild = installedBuild.map { $0.count > 6 ? String($0.prefix(6)) + "..." : $0 } let displayAvailableBuild = availableBuild.map { $0.count > 6 ? String($0.prefix(6)) + "..." : $0 } if !displayInstalledVersion.isEmpty && !displayAvailableVersion.isEmpty && displayInstalledVersion == displayAvailableVersion { var result = Text(verbatim: displayInstalledVersion).foregroundColor(.orange) if let build = displayInstalledBuild { result = result + Text(verbatim: " (\(build))").foregroundColor(ThemeColors.shared(for: colorScheme).secondaryText) } result = result + Text(verbatim: " → ") result = result + Text(verbatim: displayAvailableVersion).foregroundColor(.green) result = result + Text(verbatim: " (") if let build = displayAvailableBuild { result = result + Text(build).foregroundColor(ThemeColors.shared(for: colorScheme).secondaryText) } result = result + Text(verbatim: ")") return result } else if displayAvailableVersion.isEmpty, let availableBuild = displayAvailableBuild { var result = Text(verbatim: displayInstalledVersion).foregroundColor(.orange) if let build = displayInstalledBuild { result = result + Text(verbatim: " (\(build))").foregroundColor(ThemeColors.shared(for: colorScheme).secondaryText) } result = result + Text(verbatim: " → build ") result = result + Text(availableBuild).foregroundColor(.green) return result } else { var result = Text(verbatim: displayInstalledVersion).foregroundColor(.orange) if let build = displayInstalledBuild { result = result + Text(verbatim: " (\(build))").foregroundColor(ThemeColors.shared(for: colorScheme).secondaryText) } result = result + Text(verbatim: " → ") result = result + Text(verbatim: displayAvailableVersion).foregroundColor(.green) if let build = displayAvailableBuild { result = result + Text(verbatim: " (\(build))").foregroundColor(ThemeColors.shared(for: colorScheme).secondaryText) } return result } } else { var result = Text(verbatim: displayInstalledVersion).foregroundColor(.orange) result = result + Text(verbatim: " → ") result = result + Text(verbatim: displayAvailableVersion).foregroundColor(.green) return result } } private func buildNumberTooltip(for app: UpdateableApp) -> String? { let installedBuild = app.appInfo.appBuildNumber let availableBuild = app.availableBuildNumber // Only show tooltip if at least one build number exists and is > 6 chars guard (installedBuild?.count ?? 0) > 6 || (availableBuild?.count ?? 0) > 6 else { return nil } if app.source == .current { if let build = installedBuild { return "Build: \(build)" } } else { // Apps with updates var parts: [String] = [] if let installed = installedBuild { parts.append("Installed: \(installed)") } if let available = availableBuild { parts.append("Available: \(available)") } return parts.joined(separator: " → ") } return nil } private func preprocessChangelogText(_ text: String) -> String { let lines = text.components(separatedBy: .newlines) var processed: [String] = [] var currentLine = "" for line in lines { let trimmedLine = line.trimmingCharacters(in: .whitespaces) // Empty line = paragraph break if trimmedLine.isEmpty { if !currentLine.isEmpty { processed.append(currentLine) currentLine = "" } processed.append("") // Preserve paragraph break continue } // Check if this is a continuation line (starts with spaces in original) let isContinuation = line.hasPrefix(" ") || line.hasPrefix("\t") // Check if this starts a new item (bullet, number, or header) let startsNewItem = trimmedLine.hasPrefix("-") || trimmedLine.hasPrefix("•") || trimmedLine.hasPrefix("*") || trimmedLine.first?.isNumber == true || trimmedLine.hasSuffix(":") if isContinuation && !startsNewItem { // Join with previous line (add space if needed) if !currentLine.isEmpty && !currentLine.hasSuffix(" ") { currentLine += " " } currentLine += trimmedLine } else { // Start new line if !currentLine.isEmpty { processed.append(currentLine) } currentLine = trimmedLine } } // Don't forget the last line if !currentLine.isEmpty { processed.append(currentLine) } // Join with proper line breaks return processed.joined(separator: "\n") } private func preprocessHTML(_ html: String) -> String { var cleaned = html // Remove leading/trailing whitespace and newlines from the entire HTML cleaned = cleaned.trimmingCharacters(in: .whitespacesAndNewlines) // First pass: Remove empty list items before processing lists // Match
  • followed by only whitespace/br tags and then
  • cleaned = cleaned.replacingOccurrences(of: #"
  • (\s|)*
  • "#, with: "", options: .regularExpression) // Remove completely empty lists cleaned = cleaned.replacingOccurrences(of: #"
      (\s|)*
    "#, with: "", options: .regularExpression) cleaned = cleaned.replacingOccurrences(of: #"
      (\s|)*
    "#, with: "", options: .regularExpression) // Fix malformed structure:

    ...

    \n

    ...

    \n
      // This pattern happens in Postico where lists are split by headers cleaned = cleaned.replacingOccurrences(of: #"
    \s*()"#, with: "\n$1", options: .regularExpression) cleaned = cleaned.replacingOccurrences(of: #"()\s*
      "#, with: "$1\n
        ", options: .regularExpression) cleaned = cleaned.replacingOccurrences(of: #"\s*()"#, with: "\n$1", options: .regularExpression) cleaned = cleaned.replacingOccurrences(of: #"()\s*
          "#, with: "$1\n
            ", options: .regularExpression) // Second pass: After fixing structure, remove any newly created empty lists cleaned = cleaned.replacingOccurrences(of: #"
              (\s|)*
            "#, with: "", options: .regularExpression) cleaned = cleaned.replacingOccurrences(of: #"
              (\s|)*
            "#, with: "", options: .regularExpression) // Remove multiple consecutive
            tags (more than 2) cleaned = cleaned.replacingOccurrences(of: #"(){3,}"#, with: "

            ", options: .regularExpression) // Remove excessive whitespace between block elements (more than 1 blank line) cleaned = cleaned.replacingOccurrences(of: #"\n\s*\n\s*\n+"#, with: "\n\n", options: .regularExpression) // Clean up whitespace around list tags cleaned = cleaned.replacingOccurrences(of: #"\s*
              \s*"#, with: "
                ", options: .regularExpression) cleaned = cleaned.replacingOccurrences(of: #"\s*
              \s*"#, with: "
            ", options: .regularExpression) cleaned = cleaned.replacingOccurrences(of: #"\s*
              \s*"#, with: "
                ", options: .regularExpression) cleaned = cleaned.replacingOccurrences(of: #"\s*
              \s*"#, with: "
            ", options: .regularExpression) return cleaned } private func formattedReleaseDescription(_ description: String, for app: UpdateableApp) -> String { if app.source == .appStore { return description.replacingOccurrences(of: "\n", with: "
            ") } else { return description } } private func formatDate(_ dateString: String) -> String { // RFC 2822 format (e.g., "Mon, 17 Nov 2025 18:53:41 -0800") let rfc2822Formatter = DateFormatter() rfc2822Formatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss Z" rfc2822Formatter.locale = Locale(identifier: "en_US_POSIX") if let date = rfc2822Formatter.date(from: dateString) { let displayFormatter = DateFormatter() displayFormatter.dateStyle = .medium displayFormatter.timeStyle = .none return displayFormatter.string(from: date) } // Sparkle format (e.g., "17 November 2025 18:53:41 +0000") let sparkleFormatter = DateFormatter() sparkleFormatter.dateFormat = "dd MMMM yyyy HH:mm:ss Z" sparkleFormatter.locale = Locale(identifier: "en_US_POSIX") if let date = sparkleFormatter.date(from: dateString) { let displayFormatter = DateFormatter() displayFormatter.dateStyle = .medium displayFormatter.timeStyle = .none return displayFormatter.string(from: date) } // Common datetime format (e.g., "2021-11-18 17:06:23") let datetimeFormatter = DateFormatter() datetimeFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss" datetimeFormatter.locale = Locale(identifier: "en_US_POSIX") if let date = datetimeFormatter.date(from: dateString) { let displayFormatter = DateFormatter() displayFormatter.dateStyle = .medium displayFormatter.timeStyle = .none return displayFormatter.string(from: date) } // Date-only format (e.g., "2021-11-18") let dateOnlyFormatter = DateFormatter() dateOnlyFormatter.dateFormat = "yyyy-MM-dd" dateOnlyFormatter.locale = Locale(identifier: "en_US_POSIX") if let date = dateOnlyFormatter.date(from: dateString) { let displayFormatter = DateFormatter() displayFormatter.dateStyle = .medium displayFormatter.timeStyle = .none return displayFormatter.string(from: date) } // ISO 8601 format without separators (e.g., "2025-11-03T04:59:29Z") let compactISO8601Formatter = DateFormatter() compactISO8601Formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss'Z'" compactISO8601Formatter.locale = Locale(identifier: "en_US_POSIX") compactISO8601Formatter.timeZone = TimeZone(secondsFromGMT: 0) if let date = compactISO8601Formatter.date(from: dateString) { let displayFormatter = DateFormatter() displayFormatter.dateStyle = .medium displayFormatter.timeStyle = .none return displayFormatter.string(from: date) } // ISO 8601 format (flexible catch-all) let iso8601Formatter = ISO8601DateFormatter() iso8601Formatter.formatOptions = [.withInternetDateTime, .withDashSeparatorInDate, .withColonSeparatorInTime, .withFractionalSeconds] if let date = iso8601Formatter.date(from: dateString) { let displayFormatter = DateFormatter() displayFormatter.dateStyle = .medium displayFormatter.timeStyle = .none return displayFormatter.string(from: date) } // If all parsing fails, return original string return dateString } private func standardizeFont(in nsAttributedString: NSAttributedString) -> AttributedString { let mutableString = NSMutableAttributedString(attributedString: nsAttributedString) let textRange = NSRange(location: 0, length: mutableString.length) let systemFont = NSFont.systemFont(ofSize: NSFont.systemFontSize) // Convert SwiftUI colors to NSColor let bodyColor = NSColor(ThemeColors.shared(for: colorScheme).primaryText) let linkColor = NSColor(ThemeColors.shared(for: colorScheme).accent) // Remove all existing styling attributes mutableString.removeAttribute(.foregroundColor, range: textRange) mutableString.removeAttribute(.backgroundColor, range: textRange) mutableString.removeAttribute(.shadow, range: textRange) mutableString.removeAttribute(.font, range: textRange) // Apply base styling: system font + body text color mutableString.addAttribute(.font, value: systemFont, range: textRange) mutableString.addAttribute(.foregroundColor, value: bodyColor, range: textRange) // Preserve bold/italic traits from original HTML nsAttributedString.enumerateAttribute(.font, in: textRange, options: .reverse) { (fontObject, range, _) in guard let font = fontObject as? NSFont else { return } let traits = font.fontDescriptor.symbolicTraits let fontDescriptor = systemFont.fontDescriptor.withSymbolicTraits(traits) if let font = NSFont(descriptor: fontDescriptor, size: systemFont.pointSize) { mutableString.addAttribute(.font, value: font, range: range) } } // Apply accent color to links nsAttributedString.enumerateAttribute(.link, in: textRange, options: []) { (linkValue, range, _) in if linkValue != nil { mutableString.addAttribute(.foregroundColor, value: linkColor, range: range) } } return AttributedString(mutableString) } // MARK: - Unsupported App Content View @ViewBuilder private func unsupportedContentView(for app: UpdateableApp) -> some View { VStack(alignment: .leading, spacing: 20) { Text("This application does not have a supported installer. You may try to adopt it into Homebrew if it exists in the Cask repo.") .font(.body) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .padding(.horizontal) // Inline adoption view if isSearchingCasks || brewManager.allAvailableCasks.isEmpty { HStack { Spacer() VStack(spacing: 12) { ProgressView() .scaleEffect(1.2) Text("Searching for matching casks...") .font(.body) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } Spacer() } .frame(maxHeight: .infinity) } else { VStack(spacing: 0) { // Scrollable matching casks section ScrollView { VStack(alignment: .leading, spacing: 20) { CaskAdoptionContentView( matchingCasks: $matchingCasks, selectedCaskToken: $selectedCaskToken, manualEntry: $manualEntry, manualEntryValidation: $manualEntryValidation, adoptionError: $adoptionError, onManualEntryChange: validateManualEntry, limitCaskListHeight: false, showManualEntry: false ) } .padding(.horizontal) } .scrollIndicators(scrollIndicators ? .visible : .hidden) // Bottom-pinned manual entry and button section VStack(alignment: .leading, spacing: 12) { // Divider() // Manual entry section VStack(alignment: .leading, spacing: 8) { Text("Manual Entry") .font(.headline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Text("If the correct cask isn't listed above, enter the cask token manually:") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) HStack(spacing: 8) { TextField("e.g., firefox", text: $manualEntry) .textFieldStyle(.roundedBorder) .onChange(of: manualEntry) { newValue in validateManualEntry(newValue) } if !manualEntry.isEmpty { if let validation = manualEntryValidation { Image(systemName: "checkmark.circle.fill") .foregroundStyle(.green) .help("Valid cask: \(validation.displayName)") } else if manualEntry.count >= 2 { Image(systemName: "xmark.circle.fill") .foregroundStyle(.red) .help("Cask not found") } } } } // Adopt button HStack { Spacer() Button(isAdopting ? "Adopting..." : "Adopt with Homebrew") { performAdoption(for: app) } .buttonStyle(.borderedProminent) .disabled(isAdopting || !canAdopt) .id(app.id) } } .padding([.horizontal, .top]) .background(ThemeColors.shared(for: colorScheme).primaryBG) } } } } // MARK: - Adoption Support Methods private var canAdopt: Bool { if isSearchingCasks || isAdopting { return false } if let selected = selectedCaskToken, !selected.isEmpty { return true } if manualEntryValidation != nil, !manualEntry.isEmpty { return true } return false } private var selectedCask: AdoptableCask? { if let token = selectedCaskToken { return matchingCasks.first(where: { $0.token == token }) } return manualEntryValidation } private func loadCasksForAdoption() { guard let app = app else { return } // Load casks if not already loaded if brewManager.allAvailableCasks.isEmpty { isSearchingCasks = true Task { await brewManager.loadAvailablePackages(appState: appState) await MainActor.run { searchForMatchingCasks(for: app) } } } else { searchForMatchingCasks(for: app) } } private func searchForMatchingCasks(for app: UpdateableApp) { isSearchingCasks = true Task { try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds let matches = findMatchingCasks(for: app.appInfo, from: brewManager.allAvailableCasks) await MainActor.run { matchingCasks = matches isSearchingCasks = false // Auto-select first compatible cask if there's only one if matches.count == 1, matches[0].isVersionCompatible { selectedCaskToken = matches[0].token } } } } private func validateManualEntry(_ token: String) { guard let app = app else { return } guard !token.isEmpty, token.count >= 2 else { manualEntryValidation = nil return } let validated = validateManualCaskEntry(token, for: app.appInfo, from: brewManager.allAvailableCasks) if validated != nil { manualEntryValidation = validated selectedCaskToken = nil } else { manualEntryValidation = nil } } private func performAdoption(for app: UpdateableApp) { guard let cask = selectedCask else { return } isAdopting = true adoptionError = nil Task { do { try await HomebrewController.shared.adoptCask(token: cask.token) await brewManager.loadInstalledPackages() invalidateCaskLookupCache() let folderPaths = await MainActor.run { FolderSettingsManager.shared.folderPaths } await loadAppsAsync(folderPaths: folderPaths, useStreaming: false) await MainActor.run { isAdopting = false } // Trigger update scan to recategorize the app await UpdateManager.shared.scanIfNeeded(forceReload: true, sources: [.homebrew]) } catch { await MainActor.run { adoptionError = "Failed to adopt: \(error.localizedDescription)" isAdopting = false } } } } // MARK: - Helper Methods private func openInAppStore(urlString: String) { guard var urlComponents = URLComponents(string: urlString) else { return } urlComponents.scheme = "macappstore" if let url = urlComponents.url { NSWorkspace.shared.open(url) } } private func processedReleaseNotes(for app: UpdateableApp) -> String? { // Priority: 1) Fetched external notes, 2) Inline description guard let description = (app.fetchedReleaseNotes ?? app.releaseDescription)?.trimmingCharacters(in: .whitespacesAndNewlines) else { return nil } let htmlDescription = formattedReleaseDescription(description, for: app) // Check if content is already HTML (contains tags) let isHTML = htmlDescription.contains("<") && htmlDescription.contains(">") if isHTML { // For HTML content: just clean up malformed tags return preprocessHTML(htmlDescription) } else { // For plain text: join continuation lines and convert newlines to
            let cleaned = preprocessChangelogText(htmlDescription) return cleaned.replacingOccurrences(of: "\n", with: "
            ") } } } ================================================ FILE: Pearcleaner/Views/AppsUpdaterView/UpdateRowViewSidebar.swift ================================================ // // UpdateRowViewSidebar.swift // Pearcleaner // // Created by Alin Lupascu on 11/19/25. // import SwiftUI import AlinFoundation struct UpdateRowViewSidebar: View { let app: UpdateableApp let isSelected: Bool let onTap: () -> Void @Environment(\.colorScheme) var colorScheme @AppStorage("settings.general.glass") private var glass: Bool = true @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true @State private var isHovered: Bool = false @EnvironmentObject var updateManager: UpdateManager private var sourceColor: Color { switch app.source { case .homebrew: return .green case .appStore: return .purple case .sparkle: return .orange case .unsupported: return .gray case .current: return .gray } } private var sourceIcon: String { switch app.source { case .homebrew: return "terminal" case .appStore: return "macwindow" case .sparkle: return "sparkles" case .unsupported: return "questionmark.circle" case .current: return "app" } } var body: some View { Button(action: onTap) { HStack(alignment: .center, spacing: 12) { // App icon if let appIcon = app.appInfo.appIcon { Image(nsImage: appIcon) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 30, height: 30) .clipShape(RoundedRectangle(cornerRadius: 8)) } else { // Fallback to source icon with colored background ZStack { Circle() .fill(sourceColor.opacity(0.2)) .frame(width: 30, height: 30) Image(systemName: sourceIcon) .font(.system(size: 14, weight: .medium)) .foregroundStyle(sourceColor) } } // App name and version VStack(alignment: .leading, spacing: 2) { Text(verbatim: app.appInfo.appName) .font(.system(size: isSelected ? 14 : 12)) .lineLimit(1) .truncationMode(.tail) .frame(maxWidth: .infinity, alignment: .leading) // Version info buildVersionText(for: app, colorScheme: colorScheme) .font(.footnote) .lineLimit(1) .truncationMode(.tail) .frame(maxWidth: .infinity, alignment: .leading) } } .frame(height: 35) .background(Color.white.opacity(0.000000001)) .padding(.trailing) .padding(.vertical, 5) } .contentShape(Rectangle()) .buttonStyle(.plain) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .padding(.leading, 10) .onHover { hovering in withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.20 : 0)) { isHovered = hovering } } .background { RoundedRectangle(cornerRadius: 8) .fill( isSelected && !glass ? ThemeColors.shared(for: colorScheme).secondaryBG : .clear ) } .overlay { if isHovered || isSelected { HStack { Spacer() ZStack { // Morphing shape that animates between rectangle and square RoundedRectangle(cornerRadius: isSelected ? 6 : 50) .fill( isSelected ? ThemeColors.shared(for: colorScheme).accent : ThemeColors.shared(for: colorScheme).primaryText.opacity(0.5) ) .frame(width: isSelected ? 20 : 2, height: isSelected ? 20 : 25) if isSelected { Image(systemName: "xmark") .font(.system(size: 10, weight: .medium)) .foregroundStyle(.white) .opacity(isSelected ? 1 : 0) } } .padding(.trailing, 7) } .allowsHitTesting(false) } } .task(id: app.id) { // Fetch external release notes if we have a link but no content yet guard let notesLink = app.releaseNotesLink, let notesURL = URL(string: notesLink), app.fetchedReleaseNotes == nil else { return } await fetchReleaseNotes(for: app.id, from: notesURL) } } private func buildVersionText(for app: UpdateableApp, colorScheme: ColorScheme) -> Text { // For unsupported and current apps, just show the installed version (no arrow) if app.source == .unsupported || app.source == .current { return Text(verbatim: app.appInfo.appVersion) .foregroundColor(ThemeColors.shared(for: colorScheme).secondaryText) } guard let availableVersion = app.availableVersion else { // No available version (shouldn't happen, but handle gracefully) return Text(verbatim: app.appInfo.appVersion) } // Clean Homebrew versions for display (strip commit hash) let displayInstalledVersion = app.source == .homebrew ? app.appInfo.appVersion.stripBrewRevisionSuffix() : app.appInfo.appVersion let displayAvailableVersion = app.source == .homebrew ? availableVersion.stripBrewRevisionSuffix() : availableVersion // Simple version display without build numbers var result = Text(verbatim: displayInstalledVersion).foregroundColor(.orange) result = result + Text(verbatim: " → ").foregroundColor(ThemeColors.shared(for: colorScheme).secondaryText) result = result + Text(verbatim: displayAvailableVersion).foregroundColor(.green) return result } private func fetchReleaseNotes(for appId: UUID, from url: URL) async { do { var request = URLRequest(url: url, timeoutInterval: 3) request.httpMethod = "GET" // This runs on a background thread automatically let (data, _) = try await URLSession.shared.data(for: request) guard let content = String(data: data, encoding: .utf8) else { return } // Update the app's fetchedReleaseNotes in the UpdateManager (must be on main thread) await MainActor.run { updateManager.updateFetchedReleaseNotes(for: appId, content: content) } } catch { // Silent failure - will fall back to inline description } } } ================================================ FILE: Pearcleaner/Views/AppsUpdaterView/UpdaterDetailsSidebar.swift ================================================ // // UpdaterHiddenSidebar.swift // Pearcleaner // // Created by Alin Lupascu on 10/16/25. // import Foundation import SwiftUI import AlinFoundation // Main updater hidden sidebar view struct UpdaterDetailsSidebar: View { @Binding var hiddenSidebar: Bool @Binding var sources: UpdaterSourcesSettings @Binding var display: UpdaterDisplaySettings @EnvironmentObject var updateManager: UpdateManager @Environment(\.colorScheme) var colorScheme @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true var body: some View { if hiddenSidebar { HStack { Spacer() VStack(alignment: .leading, spacing: 14) { UpdaterSourceCheckboxSection( sources: $sources, display: $display ) Divider() UpdaterHiddenAppsSection() Spacer() UpdaterHiddenSidebarFooter() } .padding() .frame(width: 280) .ifGlassSidebar() .padding([.trailing, .bottom], 20) } .background(.black.opacity(0.00000000001)) .transition(.move(edge: .trailing)) .onTapGesture { hiddenSidebar = false } } } } // Source checkboxes section component struct UpdaterSourceCheckboxSection: View { @Binding var sources: UpdaterSourcesSettings @Binding var display: UpdaterDisplaySettings @EnvironmentObject var updateManager: UpdateManager @Environment(\.colorScheme) var colorScheme @State private var isResetting = false @State private var showResetConfirmation = false var body: some View { VStack(alignment: .leading, spacing: 12) { // Header Text("Categories") .font(.headline) .fontWeight(.semibold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) // App Store checkbox with reset button HStack(spacing: 8) { Toggle(isOn: Binding( get: { sources.appStore.enabled }, set: { newValue in sources.appStore.enabled = newValue if newValue { Task { await updateManager.scanIfNeeded(sources: [.appStore]) } } else { updateManager.updatesBySource[.appStore] = nil } } )) { HStack(spacing: 8) { Image(systemName: ifOSBelow(macOS: 14) ? "cart.fill" : "storefront.fill") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .font(.caption) .frame(width: 16) Text("App Store") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } .toggleStyle(CircleCheckboxToggleStyle()) Spacer() // Reset button Button(action: { showResetConfirmation = true }) { if isResetting { ProgressView() .controlSize(.small) } else { Image(systemName: "wrench.fill") .resizable() .scaledToFit() .frame(width: 15, height: 15) .contentShape(Rectangle()) .foregroundStyle(.blue) .help("Reset App Store (fixes stuck downloads)") } } .buttonStyle(.plain) .disabled(isResetting) .confirmationDialog( "Reset App Store?", isPresented: $showResetConfirmation, titleVisibility: .visible ) { Button("Reset", role: .destructive) { Task { await performReset() } } Button("Cancel", role: .cancel) {} } message: { Text("This will:\n• Quit App Store and related processes\n• Clear download cache\n• Fix stuck or failed downloads\n\nYou may need to sign in again.") } } // Homebrew checkbox with auto-updates toggle HStack(spacing: 8) { Toggle(isOn: Binding( get: { sources.homebrew.enabled }, set: { newValue in sources.homebrew.enabled = newValue if newValue { Task { await updateManager.scanIfNeeded(sources: [.homebrew]) } } else { updateManager.updatesBySource[.homebrew] = nil } } )) { HStack(spacing: 8) { Image(systemName: "mug") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .font(.caption) .frame(width: 16) Text("Homebrew") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } .toggleStyle(CircleCheckboxToggleStyle()) Spacer() // Auto-updates button Button(action: { sources.homebrew.showAutoUpdates.toggle() Task { await updateManager.scanIfNeeded(sources: [.homebrew]) } }) { Image(systemName: "square.and.arrow.up.fill") .resizable() .scaledToFit() .frame(width: 15, height: 15) .foregroundStyle(sources.homebrew.showAutoUpdates ? .orange : ThemeColors.shared(for: colorScheme).secondaryText) .contentShape(Rectangle()) .help(sources.homebrew.showAutoUpdates ? "Hide auto-updating apps from Homebrew" : "Show auto-updating apps in Homebrew") } .buttonStyle(.plain) } HStack(spacing: 8) { Toggle(isOn: Binding( get: { sources.sparkle.enabled }, set: { newValue in sources.sparkle.enabled = newValue if newValue { Task { await updateManager.scanIfNeeded(sources: [.sparkle]) } } else { updateManager.updatesBySource[.sparkle] = nil } } )) { HStack(spacing: 8) { Image(systemName: "sparkles") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .font(.caption) .frame(width: 16) Text("Sparkle") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } .toggleStyle(CircleCheckboxToggleStyle()) Spacer() // Pre-releases button Button(action: { // let wasEnabled = sources.sparkle.includePreReleases sources.sparkle.includePreReleases.toggle() if sources.sparkle.includePreReleases { // Enabling - rescan to find pre-releases Task { await updateManager.scanIfNeeded(sources: [.sparkle]) } } else { // Disabling - filter existing data without rescanning updateManager.removePreReleaseApps(from: .sparkle) } }) { if #available(macOS 14.0, *) { Image(systemName: sources.sparkle.includePreReleases ? "flask.fill" : "flask") .resizable() .scaledToFit() .frame(width: 15, height: 15) .foregroundStyle(sources.sparkle.includePreReleases ? .green : ThemeColors.shared(for: colorScheme).secondaryText) .contentShape(Rectangle()) .help(sources.sparkle.includePreReleases ? "Disable pre-releases" : "Enable pre-releases") } else { Image(systemName: "testtube.2") .resizable() .scaledToFit() .frame(width: 15, height: 15) .foregroundStyle(sources.sparkle.includePreReleases ? .green : ThemeColors.shared(for: colorScheme).secondaryText) .contentShape(Rectangle()) .help(sources.sparkle.includePreReleases ? "Disable pre-releases" : "Enable pre-releases") } } .buttonStyle(.plain) } // Current apps toggle Toggle(isOn: Binding( get: { display.showCurrent }, set: { newValue in display.showCurrent = newValue } )) { HStack(spacing: 8) { Image(systemName: "checkmark.circle") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .font(.caption) .frame(width: 16) Text("Current") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } .toggleStyle(CircleCheckboxToggleStyle()) .help("Show apps that are already up-to-date") // Unsupported apps toggle Toggle(isOn: Binding( get: { display.showUnsupported }, set: { newValue in display.showUnsupported = newValue } )) { HStack(spacing: 8) { Image(systemName: "questionmark.circle") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .font(.caption) .frame(width: 16) Text("Unsupported") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } .toggleStyle(CircleCheckboxToggleStyle()) .help("Show apps without a supported update mechanism") } } // MARK: - Private Methods /// Performs App Store reset operation private func performReset() async { isResetting = true let result = await AppStoreReset.reset() await MainActor.run { isResetting = false switch result { case .success: // Show success notification showToast("App Store reset successfully", type: .success) // Optionally rescan for updates after reset Task { try? await Task.sleep(nanoseconds: 1_000_000_000) // Wait 1 second await updateManager.scanIfNeeded(forceReload: true, sources: [.appStore]) } case .failure(let error): // Show error notification showToast("Reset failed: \(error)", type: .error) } } } /// Shows a toast notification private func showToast(_ message: String, type: ToastType) { // Use AppState's toast system if available NotificationCenter.default.post( name: NSNotification.Name("ToastNotification"), object: nil, userInfo: ["message": message, "type": type.rawValue] ) } /// Toast notification types private enum ToastType: String { case success case error } } // Hidden apps section component (combines header, list, and manual hide) struct UpdaterHiddenAppsSection: View { @StateObject private var appState = AppState.shared @EnvironmentObject var updateManager: UpdateManager @Environment(\.colorScheme) var colorScheme private var availableApps: [AppInfo] { let hiddenBundleIds = Set(updateManager.hiddenUpdates.map { $0.appInfo.bundleIdentifier }) return appState.sortedApps.filter { !hiddenBundleIds.contains($0.bundleIdentifier) } .sorted { $0.appName.localizedCaseInsensitiveCompare($1.appName) == .orderedAscending } } var body: some View { VStack(alignment: .leading, spacing: 8) { // Header with count and plus button HStack(spacing: 8) { Text("Hidden") .font(.headline) .fontWeight(.semibold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Text(verbatim: "(\(updateManager.hiddenUpdates.count))") .font(.headline) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() Menu { if availableApps.isEmpty { Text("All updates are hidden") .disabled(true) } else { ForEach(availableApps, id: \.bundleIdentifier) { app in Button { hideApp(app) } label: { HStack { if let icon = app.appIcon { Image(nsImage: icon) .resizable() .frame(width: 16, height: 16) } Text(app.appName) } } } } } label: { Image(systemName: "plus.circle.fill") .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) .font(.title3) } .menuStyle(.borderlessButton) .menuIndicator(.hidden) .help("Manually hide an app from update checks") } // List of hidden apps if updateManager.hiddenUpdates.isEmpty { Text("No hidden updates") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .italic() .padding(.top, 4) } else { ScrollView { LazyVStack(alignment: .leading, spacing: 4) { ForEach(updateManager.hiddenUpdates) { app in UpdaterHiddenAppRow(app: app) } } } } } } private func hideApp(_ appInfo: AppInfo) { // Determine the update source for this app let source: UpdateSource = { if appInfo.hasSparkle { return .sparkle } else if appInfo.brew { return .homebrew } else if appInfo.isAppStore { return .appStore } else { return .unsupported } }() // Create UpdateableApp instance let updateableApp = UpdateableApp( appInfo: appInfo, availableVersion: nil, availableBuildNumber: nil, source: source, adamID: nil, appStoreURL: nil, status: .idle, progress: 0.0, isSelectedForUpdate: false, releaseTitle: nil, releaseDescription: nil, releaseNotesLink: nil, releaseDate: nil, isPreRelease: false, isIOSApp: false, foundInRegion: nil, appcastItem: nil ) updateManager.hideApp(updateableApp) } } // Individual hidden app row component struct UpdaterHiddenAppRow: View { let app: UpdateableApp @EnvironmentObject var updateManager: UpdateManager @Environment(\.colorScheme) var colorScheme @State private var isHovered = false private var sourceIcon: String { switch app.source { case .appStore: return ifOSBelow(macOS: 14) ? "cart.fill" : "storefront.fill" case .homebrew: return "mug" case .sparkle: return "sparkles" case .unsupported: return "questionmark.circle" case .current: return "checkmark.circle" } } private var sourceColor: Color { switch app.source { case .appStore: return .blue case .homebrew: return .orange case .sparkle: return .purple case .unsupported: return .gray case .current: return .green } } var body: some View { HStack(spacing: 8) { // App icon (use actual icon if available, fallback to source icon) if let appIcon = app.appInfo.appIcon { Image(nsImage: appIcon) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 32, height: 32) .cornerRadius(6) } else { // Fallback to source icon with colored background ZStack { Circle() .fill(sourceColor.opacity(0.2)) .frame(width: 32, height: 32) Image(systemName: sourceIcon) .font(.system(size: 14, weight: .medium)) .foregroundStyle(sourceColor) } } VStack(alignment: .leading, spacing: 2) { HStack(spacing: 4) { Text(app.appInfo.appName) .lineLimit(1) if let ignoredVersion = updateManager.getIgnoredVersion(for: app) { Text("(Skipped \(ignoredVersion))") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } else { Text("(Hidden)") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) HStack(spacing: 4) { Image(systemName: sourceIcon) .font(.caption2) .foregroundStyle(sourceColor) Text(app.source.rawValue.capitalized) .font(.caption2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } Spacer() // Unhide button Button { Task { await updateManager.unhideApp(app) } } label: { Image(systemName: "eye") .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) } .buttonStyle(.borderless) .help("Unhide update") } .padding(8) .background(ThemeColors.shared(for: colorScheme).secondaryText.opacity(isHovered ? 0.15 : 0.1)) .cornerRadius(6) .onHover { hovered in withAnimation(.easeInOut(duration: 0.2)) { isHovered = hovered } } } } // Footer component struct UpdaterHiddenSidebarFooter: View { @Environment(\.colorScheme) var colorScheme @EnvironmentObject var updateManager: UpdateManager @AppStorage("settings.updater.debugLogging") private var debugLogging: Bool = true var body: some View { HStack { Text("Click to dismiss") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() Toggle(isOn: Binding( get: { debugLogging }, set: { newValue in debugLogging = newValue if !newValue { UpdaterDebugLogger.shared.clearLogs() } else { Task { await updateManager.scanIfNeeded() } } } )) { Text("Debug") .font(.caption) .foregroundStyle(.orange) } .toggleStyle(CircleCheckboxToggleStyle()) .help("Enable verbose logging and bundle cache flushing for troubleshooting") } } } ================================================ FILE: Pearcleaner/Views/AppsView/AppListItems.swift ================================================ // // AppListDetails.swift // Pearcleaner // // Created by Alin Lupascu on 11/10/23. // import AlinFoundation import Foundation import SwiftUI struct AppListItems: View { @EnvironmentObject var appState: AppState @Binding var search: String @State private var isHovered = false @Environment(\.colorScheme) var colorScheme @AppStorage("settings.general.glass") private var glass: Bool = true @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true @AppStorage("settings.interface.minimalist") private var minimalEnabled: Bool = true @AppStorage("settings.interface.multiSelect") private var multiSelect: Bool = false @EnvironmentObject var locations: Locations let itemId = UUID() let appInfo: AppInfo var isSelected: Bool { appState.appInfo.path == appInfo.path } @State private var hoveredItemPath: URL? = nil @Namespace var appItemNamespace var body: some View { HStack { if multiSelect { Button { let isChecked = self.appState.externalPaths.contains(self.appInfo.path) if !isChecked { if !self.appState.externalPaths.contains(self.appInfo.path) { let wasEmpty = self.appState.externalPaths.isEmpty self.appState.externalPaths.append(self.appInfo.path) if wasEmpty && appState.currentView != .files { appState.multiMode = true showAppInFiles( appInfo: appInfo, appState: appState, locations: locations) } } } else { self.appState.externalPaths.removeAll { $0 == self.appInfo.path } if self.appState.externalPaths.isEmpty { appState.multiMode = false } } } label: { EmptyView() } .buttonStyle(CircleCheckboxButtonStyle(isSelected: self.appState.externalPaths.contains(self.appInfo.path))) .padding(.leading) } Button(action: { if !isSelected { showAppInFiles(appInfo: appInfo, appState: appState, locations: locations) } else { // Closing the same item updateOnMain { appState.appInfo = .empty appState.selectedItems = [] appState.currentView = .empty } } }) { VStack { HStack(alignment: .center) { if let appIcon = appInfo.appIcon { ZStack { Image(nsImage: appIcon) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 30) .clipShape(RoundedRectangle(cornerRadius: 8)) .matchedGeometryEffect( id: "icon-\(appInfo.path.path)", in: appItemNamespace) } } if minimalEnabled { Text(verbatim: appInfo.appName) .font(.system(size: (isSelected) ? 14 : 12)) .lineLimit(1) .truncationMode(.tail) } else { VStack(alignment: .center, spacing: 2) { HStack { Text(verbatim: appInfo.appName) .font(.system(size: (isSelected) ? 14 : 12)) .lineLimit(1) .truncationMode(.tail) Spacer() } HStack(spacing: 5) { Text(verbatim: "v\(appInfo.appVersion)") .font(.footnote) .lineLimit(1) .truncationMode(.tail) .opacity(0.5) Text(verbatim: "•").font(.footnote).opacity(0.5) Text( appInfo.bundleSize == 0 ? String(localized: "calculating") : "\(formatByte(size: appInfo.bundleSize).human)" ) .font(.footnote) .lineLimit(1) .truncationMode(.tail) .opacity(0.5) .frame(minWidth: 60, alignment: .leading) Spacer() } } } if minimalEnabled { Spacer() } if minimalEnabled && !isSelected { Text( appInfo.bundleSize == 0 ? "" : formatByte(size: appInfo.bundleSize).human ) .font(.system(size: 10)) .foregroundStyle( ThemeColors.shared(for: colorScheme).primaryText.opacity(0.5)) .frame(minWidth: 60, alignment: .trailing) } } } .frame(height: 35) .background(Color.white.opacity(0.000000001)) .padding(.trailing) .padding(.vertical, 5) } .contentShape(Rectangle()) .buttonStyle(.plain) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .padding(.leading, multiSelect ? 0 : 10) .onHover { hovering in withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.20 : 0)) { self.isHovered = hovering self.hoveredItemPath = isHovered ? appInfo.path : nil } } } .background { RoundedRectangle(cornerRadius: 8) .fill( isSelected && !glass ? ThemeColors.shared(for: colorScheme).secondaryBG : .clear ) } .overlay { if isHovered || isSelected { HStack { Spacer() ZStack { // Morphing shape that animates between rectangle and square RoundedRectangle(cornerRadius: isSelected ? 6 : 50) .fill( isSelected ? ThemeColors.shared(for: colorScheme).accent : ThemeColors.shared(for: colorScheme).primaryText.opacity(0.5) ) .frame(width: isSelected ? 20 : 2, height: isSelected ? 20 : 25) if isSelected { Image(systemName: "xmark") .font(.system(size: 10, weight: .medium)) .foregroundStyle(.white) .opacity(isSelected ? 1 : 0) } } // .animation(animationEnabled ? .spring(response: 0.4, dampingFraction: 0.7, blendDuration: 0) : .linear(duration: 0), value: isSelected) .padding(.trailing, 7) } .allowsHitTesting(false) } } .onAppear { if appInfo.bundleSize == 0 { appState.getBundleSize(for: appInfo) { size in // printOS("Getting size for: \(appInfo.appName)") // appInfo.bundleSize = size } } } } } ================================================ FILE: Pearcleaner/Views/AppsView/AppSearchView.swift ================================================ // // Searchbar.swift // Pearcleaner // // Created by Alin Lupascu on 4/26/24. // import AlinFoundation import SwiftUI struct AppSearchView: View { @EnvironmentObject var appState: AppState @EnvironmentObject var locations: Locations @EnvironmentObject var fsm: FolderSettingsManager @EnvironmentObject var updater: Updater @EnvironmentObject var permissionManager: PermissionManager @Environment(\.colorScheme) var colorScheme @State private var search: String = "" @AppStorage("settings.general.selectedSortAppsList") var selectedSortOption: SortOption = .alphabetical @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true @AppStorage("settings.interface.multiSelect") private var multiSelect: Bool = false @AppStorage("settings.general.sidebarWidth") private var sidebarWidth: Double = 265 @State private var dimensionStart: Double? var body: some View { VStack(alignment: .center, spacing: 0) { if appState.sortedApps.isEmpty { VStack { Spacer() Text("No apps found") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .font(.callout) Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) } else { SearchBarSidebar(search: $search) .padding() .padding(.top, 20) if !filteredApps.isEmpty { AppsListView( search: $search, filteredApps: filteredApps, isGridMode: appState.isGridMode ) .padding([.bottom, .horizontal], 5) } else { VStack { Spacer() Text("No results") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .font(.title2) Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) } } } .onAppear { // Initialize grid mode based on current sidebar width appState.isGridMode = sidebarWidth > 316 } .onChange(of: sidebarWidth) { newWidth in // Update grid mode when sidebar width changes programmatically let newGridMode = newWidth > 316 if newGridMode != appState.isGridMode { withAnimation(.easeInOut(duration: animationEnabled ? 0.3 : 0)) { appState.isGridMode = newGridMode } } } .overlay(alignment: .trailing) { // Invisible resize handle on the trailing edge Rectangle() .fill(Color.clear) .frame(width: 10) .contentShape(Rectangle()) .offset(x: 5) // Center on the edge .onHover { inside in if inside { NSCursor.resizeLeftRight.push() } else { NSCursor.pop() } } .contextMenu { Button("Reset Size") { sidebarWidth = 265 appState.isGridMode = false } } .gesture(sidebarDragGesture) .help("Right click to reset size") } } private var sidebarDragGesture: some Gesture { DragGesture(minimumDistance: 5, coordinateSpace: .global) .onChanged { val in if dimensionStart == nil { dimensionStart = sidebarWidth } let delta = val.location.x - val.startLocation.x let newDimension = dimensionStart! + Double(delta) // Calculate dynamic max width based on available items let filteredUserApps = filteredApps.filter { !$0.system } let filteredSystemApps = filteredApps.filter { $0.system } let maxItemsInSection = max(filteredUserApps.count, filteredSystemApps.count) let optimalColumns = min(5, max(1, maxItemsInSection)) // Calculate ideal width for optimal columns (item width + spacing + padding) let idealMaxWidth = Double(optimalColumns * 120 + (optimalColumns - 1) * 5 + 50) // Extended range with dynamic max, but always allow grid mode at 316+ let minWidth: Double = 240 let maxWidth: Double = max(400, min(640, idealMaxWidth)) // Always allow at least 400px for grid mode let newWidth = max(minWidth, min(maxWidth, newDimension)) sidebarWidth = newWidth // Toggle grid mode at 316px threshold (around 3 columns) let newGridMode = newWidth > 316 if newGridMode != appState.isGridMode { withAnimation(.easeInOut(duration: animationEnabled ? 0.3 : 0)) { appState.isGridMode = newGridMode } } NSCursor.closedHand.set() } .onEnded { val in dimensionStart = nil NSCursor.arrow.set() } } private var filteredApps: [AppInfo] { let apps: [AppInfo] if search.isEmpty { apps = appState.sortedApps } else { // Use custom fuzzy search algorithm let searchResults = appState.sortedApps.fuzzySearch(query: search) apps = searchResults.map { $0.item } } // Sort based on the selected option switch selectedSortOption { case .alphabetical: return apps.sorted { $0.appName.replacingOccurrences(of: ".", with: "").sortKey < $1.appName.replacingOccurrences(of: ".", with: "").sortKey } case .size: return apps.sorted { $0.bundleSize > $1.bundleSize } case .creationDate: return apps.sorted { ($0.creationDate ?? Date.distantPast) > ($1.creationDate ?? Date.distantPast) } case .dateAdded: return apps.sorted { ($0.dateAdded ?? Date.distantPast) > ($1.dateAdded ?? Date.distantPast) } case .contentChangeDate: return apps.sorted { ($0.contentChangeDate ?? Date.distantPast) > ($1.contentChangeDate ?? Date.distantPast) } case .lastUsedDate: return apps.sorted { ($0.lastUsedDate ?? Date.distantPast) > ($1.lastUsedDate ?? Date.distantPast) } } } } struct SearchBarSidebar: View { @Binding var search: String @State var menu: Bool = true @State var padding: CGFloat = 5 @State var sidebar: Bool = true @EnvironmentObject var appState: AppState var body: some View { HStack { TextField("Search...", text: $search) .textFieldStyle(SimpleSearchStyleSidebar(menu: menu, trash: true, text: $search)) } } } ================================================ FILE: Pearcleaner/Views/AppsView/AppsListView.swift ================================================ // // AppsListView.swift // Pearcleaner // // Created by Alin Lupascu on 3/4/24. // import Foundation import SwiftUI struct AppsListView: View { @Binding var search: String @AppStorage("settings.general.selectedSort") var selectedSortAlpha: Bool = true @AppStorage("settings.interface.scrollIndicators") private var scrollIndicators: Bool = false var filteredApps: [AppInfo] var isGridMode: Bool var body: some View { ScrollView { if isGridMode { LazyVStack(alignment: .leading, spacing: 10) { let filteredUserApps = filteredApps.filter { !$0.system } let filteredSystemApps = filteredApps.filter { $0.system } let maxColumns = 5 // Maximum columns allowed let maxItemsInSection = max(filteredUserApps.count, filteredSystemApps.count) let optimalColumns = min(maxColumns, max(1, maxItemsInSection)) if !filteredUserApps.isEmpty { GridSectionView( title: String(localized: "User"), count: filteredUserApps.count, apps: filteredUserApps, search: $search, maxColumns: min(optimalColumns, filteredUserApps.count)) } if !filteredSystemApps.isEmpty { GridSectionView( title: String(localized: "System"), count: filteredSystemApps.count, apps: filteredSystemApps, search: $search, maxColumns: min(optimalColumns, filteredSystemApps.count)) } } .padding(.horizontal, 5) } else { VStack(alignment: .leading, spacing: 0) { let filteredUserApps = filteredApps.filter { !$0.system } let filteredSystemApps = filteredApps.filter { $0.system } if !filteredUserApps.isEmpty { SectionView( title: String(localized: "User"), count: filteredUserApps.count, apps: filteredUserApps, search: $search) } if !filteredSystemApps.isEmpty { SectionView( title: String(localized: "System"), count: filteredSystemApps.count, apps: filteredSystemApps, search: $search ) .padding(.top, 5) } } } } .scrollIndicators(scrollIndicators ? .automatic : .never) } } struct SectionView: View { var title: String var count: Int var apps: [AppInfo] @Binding var search: String @State private var showItems: Bool = true @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true var body: some View { VStack(spacing: 0) { Header(title: title, count: count) .padding(.leading, 5) .onTapGesture { withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { showItems.toggle() } } if showItems { ForEach(apps, id: \.self) { appInfo in AppListItems(search: $search, appInfo: appInfo) .transition(.opacity) } } } } } struct Header: View { let title: String let count: Int @EnvironmentObject var appState: AppState @EnvironmentObject var locations: Locations @EnvironmentObject var fsm: FolderSettingsManager @Environment(\.colorScheme) var colorScheme @AppStorage("settings.general.glass") private var glass: Bool = true var body: some View { HStack { Text(verbatim: "\(title)").foregroundStyle( ThemeColors.shared(for: colorScheme).primaryText ).opacity(0.5) Text(verbatim: "\(count)") .font(.system(size: 10)) .monospacedDigit() .frame(width: count > 99 ? 30 : 24, height: 17) .background(ThemeColors.shared(for: colorScheme).primaryText.opacity(0.1)) .clipShape(.capsule) .padding(.leading, 2) Spacer() } .background(.black.opacity(0.0000000001)) .frame(minHeight: 20) .padding([.horizontal, .bottom], 5) } } struct GridSectionView: View { var title: String var count: Int var apps: [AppInfo] @Binding var search: String var maxColumns: Int @State private var showItems: Bool = true @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true var body: some View { VStack(spacing: 5) { Header(title: title, count: count) .padding(.leading, 5) .onTapGesture { withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { showItems.toggle() } } if showItems { LazyVGrid( columns: [ GridItem(.adaptive(minimum: 120, maximum: 120), spacing: 5) ], spacing: 5 ) { ForEach(apps, id: \.self) { appInfo in GridAppItem(search: $search, appInfo: appInfo) .transition(.opacity) } } .frame(maxWidth: CGFloat(maxColumns * 120 + (maxColumns - 1) * 5 + 10)) } } } } //List { // Section { // ForEach(filteredUserApps, id:\.self) { element in // Text(element.appName) // } // .listRowBackground(Color.clear) // } header: { // Text("User Apps") // } // // Section { // ForEach(filteredSystemApps, id:\.self) { element in // Text(element.appName) // } // .listRowBackground(Color.clear) // } header: { // Text("System Apps") // } // //} //.scrollContentBackground(.hidden) //.background(.clear) //.scrollIndicators(.never) ================================================ FILE: Pearcleaner/Views/AppsView/GridAppItem.swift ================================================ // // GridAppItem.swift // Pearcleaner // // Created for grid layout mode // import AlinFoundation import Foundation import SwiftUI struct GridAppItem: View { @EnvironmentObject var appState: AppState @Binding var search: String @State private var isHovered = false @Environment(\.colorScheme) var colorScheme @AppStorage("settings.general.glass") private var glass: Bool = true @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true @AppStorage("settings.interface.multiSelect") private var multiSelect: Bool = false @EnvironmentObject var locations: Locations let itemId = UUID() let appInfo: AppInfo var isSelected: Bool { appState.appInfo.path == appInfo.path } @State private var hoveredItemPath: URL? = nil @Namespace var appItemNamespace var body: some View { VStack(spacing: 8) { if multiSelect { HStack { Button { let isChecked = self.appState.externalPaths.contains(self.appInfo.path) if !isChecked { if !self.appState.externalPaths.contains(self.appInfo.path) { let wasEmpty = self.appState.externalPaths.isEmpty self.appState.externalPaths.append(self.appInfo.path) if wasEmpty && appState.currentView != .files { appState.multiMode = true showAppInFiles( appInfo: appInfo, appState: appState, locations: locations) } } } else { self.appState.externalPaths.removeAll { $0 == self.appInfo.path } if self.appState.externalPaths.isEmpty { appState.multiMode = false } } } label: { EmptyView() } .buttonStyle(CircleCheckboxButtonStyle(isSelected: self.appState.externalPaths.contains(self.appInfo.path))) Spacer() } } Button(action: { if !isSelected { updateOnMain { appState.appInfo = .empty appState.selectedItems = [] appState.currentView = .empty } withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { showAppInFiles(appInfo: appInfo, appState: appState, locations: locations) } } else { // Closing the same item withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { updateOnMain { appState.appInfo = .empty appState.selectedItems = [] appState.currentView = .empty } } } }) { VStack(spacing: 6) { // App Icon if let appIcon = appInfo.appIcon { ZStack { Image(nsImage: appIcon) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 48, height: 48) .clipShape(RoundedRectangle(cornerRadius: 12)) .matchedGeometryEffect( id: "icon-\(appInfo.path.path)", in: appItemNamespace) } } // App Name Text(appInfo.appName) .font(.system(size: 12, weight: isSelected ? .medium : .regular)) .lineLimit(2) .truncationMode(.tail) .multilineTextAlignment(.center) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) // Size info Text( appInfo.bundleSize == 0 ? "..." : formatByte(size: appInfo.bundleSize).human ) .font(.system(size: 9)) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText.opacity(0.6)) .lineLimit(1) .frame(minWidth: 50, alignment: .center) } } .buttonStyle(.borderless) .frame(maxWidth: .infinity, maxHeight: .infinity) .contentShape(RoundedRectangle(cornerRadius: 12)) .onHover { hovering in withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.20 : 0)) { self.isHovered = hovering self.hoveredItemPath = isHovered ? appInfo.path : nil } } Spacer(minLength: 0) } .frame(maxWidth: .infinity) .padding(8) .background { RoundedRectangle(cornerRadius: 12) .fill( isSelected && !glass ? ThemeColors.shared(for: colorScheme).secondaryBG : .clear ) } .overlay { if isSelected && isHovered { // Dimmed background with centered X when selected and hovered ZStack { RoundedRectangle(cornerRadius: 12) .fill(ThemeColors.shared(for: colorScheme).primaryBG.opacity(0.8)) ZStack { Circle() .fill(ThemeColors.shared(for: colorScheme).secondaryBG.opacity(0.9)) .frame(width: 32, height: 32) Image(systemName: "xmark") .font(.system(size: 14, weight: .medium)) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } } .allowsHitTesting(false) } else if isSelected { // Just selected, no hover - show subtle selection indicator RoundedRectangle(cornerRadius: 12) .strokeBorder(ThemeColors.shared(for: colorScheme).accent, lineWidth: 2) .allowsHitTesting(false) } else if isHovered { // Just hovered, not selected RoundedRectangle(cornerRadius: 12) .strokeBorder( ThemeColors.shared(for: colorScheme).primaryText.opacity(0.3), lineWidth: 1 ) .allowsHitTesting(false) } } .onAppear { if appInfo.bundleSize == 0 { appState.getBundleSize(for: appInfo) { size in // printOS("Getting size for: \(appInfo.appName)") // appInfo.bundleSize = size } } } } } ================================================ FILE: Pearcleaner/Views/Brew/AutoUpdateSection.swift ================================================ // // AutoUpdateSection.swift // Pearcleaner // // Created by Alin Lupascu on 10/25/25. // import SwiftUI import AlinFoundation // MARK: - Auto Update Status enum AutoUpdateStatus { case disabled // Master toggle OFF case pending // Master toggle ON, but no active schedules case active(Int) // Master toggle ON, LaunchAgent running with X schedules } struct AutoUpdateSection: View { @ObservedObject private var manager = HomebrewAutoUpdateManager.shared @Environment(\.colorScheme) var colorScheme @State private var errorMessage: String? @State private var showError: Bool = false @State private var logSheetWindow: NSWindow? @AppStorage("settings.interface.scrollIndicators") private var scrollIndicators: Bool = false // Computed status based on master toggle, plist state, schedules, and agent private var currentStatus: AutoUpdateStatus { // Primary check: Master toggle state if !manager.isEnabled { return .disabled } // Secondary check: Count enabled schedules let enabledCount = manager.schedules.filter { $0.isEnabled }.count if enabledCount > 0 && manager.isAgentLoaded { return .active(enabledCount) } // Tertiary check: Enabled but no active schedules or agent not loaded return .pending } var body: some View { VStack(spacing: 0) { ScrollView { VStack(alignment: .leading, spacing: 20) { // Status Section GroupBox { VStack(alignment: .leading, spacing: 12) { HStack { VStack(alignment: .leading, spacing: 4) { HStack(spacing: 6) { Text("Status") .font(.headline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) // Status icon based on current state switch currentStatus { case .disabled: Image(systemName: "xmark.circle.fill") .foregroundStyle(.red) .font(.callout) .help("Automatic updates are disabled") case .pending: Image(systemName: "clock.fill") .foregroundStyle(.orange) .font(.callout) .help("Waiting for schedules to be configured") case .active(_): Image(systemName: "checkmark.circle.fill") .foregroundStyle(.green) .font(.callout) .help("LaunchAgent is active") } } // Status text based on current state switch currentStatus { case .disabled: Text("Disabled") .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) case .pending: Text("Pending schedules") .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) case .active(let count): Text("\(count) schedule\(count == 1 ? "" : "s") active") .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } Spacer() // Master enable/disable toggle Toggle("", isOn: Binding( get: { manager.isEnabled }, set: { newValue in do { try manager.toggleEnabled(newValue) } catch { errorMessage = error.localizedDescription showError = true } } )) .toggleStyle(.switch) .controlSize(.large) .help(manager.isEnabled ? "Disable automatic updates (preserves schedule)" : "Enable automatic updates") } Divider() // Actions Section VStack(alignment: .leading, spacing: 8) { Text("Actions (applied to all schedules)") .font(.subheadline) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) HStack(spacing: 20) { Toggle("Update Homebrew", isOn: $manager.runUpdate) .toggleStyle(CircleCheckboxToggleStyle()) .help("Update Homebrew itself") .disabled(!manager.isEnabled) .onChange(of: manager.runUpdate) { _ in if manager.isEnabled && !manager.schedules.isEmpty { Task { do { try manager.applySchedule() manager.originalRunUpdate = manager.runUpdate } catch { errorMessage = error.localizedDescription showError = true } } } } Toggle("Upgrade Packages", isOn: $manager.runUpgrade) .toggleStyle(CircleCheckboxToggleStyle()) .help("Upgrade currently installed packages") .disabled(!manager.isEnabled) .onChange(of: manager.runUpgrade) { _ in if manager.isEnabled && !manager.schedules.isEmpty { Task { do { try manager.applySchedule() manager.originalRunUpgrade = manager.runUpgrade } catch { errorMessage = error.localizedDescription showError = true } } } } Toggle("Cleanup", isOn: $manager.runCleanup) .toggleStyle(CircleCheckboxToggleStyle()) .help("Remove orphaned dependencies, old package versions and scrub download cache") .disabled(!manager.isEnabled) .onChange(of: manager.runCleanup) { _ in if manager.isEnabled && !manager.schedules.isEmpty { Task { do { try manager.applySchedule() manager.originalRunCleanup = manager.runCleanup } catch { errorMessage = error.localizedDescription showError = true } } } } } } } .padding() } // Schedule Section GroupBox { VStack(alignment: .leading, spacing: 12) { HStack { Text("Schedule") .font(.headline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Spacer() // View Log button if manager.logFileExists { Button { showLogSheet() } label: { Label("View Log", systemImage: "doc.text") } .buttonStyle(.bordered) .help("View homebrew-autoupdate.log") } Button { manager.schedules.append(ScheduleOccurrence()) } label: { Label("Add Schedule", systemImage: "plus") .font(.callout) } .help("Add a new schedule occurrence") .disabled(!manager.isEnabled) } if manager.schedules.isEmpty { VStack(spacing: 8) { Text("No schedules configured") .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) if manager.isEnabled { Text("Add a schedule to activate automatic updates") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } .frame(maxWidth: .infinity, alignment: .center) .padding(.vertical, 20) } else { Divider() LazyVStack(spacing: 8) { ForEach($manager.schedules) { $schedule in ScheduleRow( schedule: $schedule, isDisabled: !manager.isEnabled, isInEditMode: manager.isInEditMode(schedule), isNewSchedule: !manager.originalSchedules.contains(where: { $0.id == schedule.id }) ) } } } } .padding() } } .padding() } .scrollIndicators(scrollIndicators ? .automatic : .never) } .alert("Error", isPresented: $showError) { Button("Okay", role: .cancel) {} } message: { Text(errorMessage ?? "Unknown error") } .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("HomebrewViewShouldRefresh"))) { _ in manager.refreshState() } .onDisappear { manager.removeUnsavedSchedules() } } // MARK: - Log Viewer Sheet private func showLogSheet() { // Read log file guard let logContent = try? String(contentsOfFile: manager.logPath, encoding: .utf8) else { errorMessage = "Could not read log file" showError = true return } guard let parentWindow = NSApp.keyWindow ?? NSApp.windows.first(where: { $0.isVisible }) else { return } // Create SwiftUI view let contentView = LogViewerSheet( logContent: logContent, onClose: { if let sheetWindow = self.logSheetWindow { parentWindow.endSheet(sheetWindow) } self.logSheetWindow = nil } ) // Create sheet window let hostingController = NSHostingController(rootView: contentView) let sheetWindow = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 700, height: 500), styleMask: [.titled, .closable, .resizable], backing: .buffered, defer: false ) sheetWindow.title = "Log Viewer" sheetWindow.contentViewController = hostingController sheetWindow.isReleasedWhenClosed = false // Present as sheet parentWindow.beginSheet(sheetWindow) self.logSheetWindow = sheetWindow } } // MARK: - Schedule Row Component struct ScheduleRow: View { @Binding var schedule: ScheduleOccurrence let isDisabled: Bool let isInEditMode: Bool let isNewSchedule: Bool @Environment(\.colorScheme) var colorScheme @State private var errorMessage: String? @State private var showError: Bool = false private let weekdayNames = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] // Convert 24-hour to 12-hour format private var hour12: Int { let h = schedule.hour if h == 0 { return 12 } if h > 12 { return h - 12 } return h } private var isPM: Bool { schedule.hour >= 12 } var body: some View { HStack(spacing: 8) { // Toggle - outside background Toggle("", isOn: $schedule.isEnabled) .toggleStyle(.switch) .controlSize(.mini) .help(schedule.isEnabled ? "Schedule is enabled" : "Schedule is disabled") .onChange(of: schedule.isEnabled) { _ in // Instant save for toggle changes (don't trigger edit mode) Task { do { try HomebrewAutoUpdateManager.shared.applySchedule() } catch { await MainActor.run { errorMessage = error.localizedDescription showError = true } } } } // Rest of row - with background HStack(spacing: 5) { // Frequency picker Picker("", selection: $schedule.frequency) { Text("Daily").tag(ScheduleFrequency.daily) Text("Weekly").tag(ScheduleFrequency.weekly) Text("Monthly").tag(ScheduleFrequency.monthly) } .labelsHidden() .minimalistPicker() .frame(width: 70, alignment: .center) Divider() .padding(.trailing, 12) // Weekday picker (only for weekly schedules) if schedule.frequency == .weekly { Picker("", selection: Binding( get: { schedule.weekday ?? 0 }, set: { schedule.weekday = $0 } )) { ForEach(0..<7) { day in Text(weekdayNames[day]).tag(day) } } .labelsHidden() .minimalistPicker() .frame(width: 75, alignment: .center) Divider() .padding(.trailing, 12) } // Day of month picker (only for monthly schedules) if schedule.frequency == .monthly { Picker("", selection: Binding( get: { schedule.dayOfMonth ?? 1 }, set: { schedule.dayOfMonth = $0 } )) { ForEach(1...28, id: \.self) { day in Text(String(day)).tag(day) } } .labelsHidden() .minimalistPicker() .frame(width: 60, alignment: .center) Divider() .padding(.trailing, 12) } HStack(spacing: 2) { // Hour picker (12-hour format) Picker("", selection: Binding( get: { hour12 }, set: { newHour in // Convert back to 24-hour if isPM { schedule.hour = newHour == 12 ? 12 : newHour + 12 } else { schedule.hour = newHour == 12 ? 0 : newHour } } )) { ForEach(1...12, id: \.self) { hour in Text(verbatim: String(format: "%02d", hour)).tag(hour) .monospacedDigit() } } .labelsHidden() .minimalistPicker() .frame(alignment: .center) .fixedSize() Text(verbatim: ":") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .font(.title3) // Minute picker Picker("", selection: $schedule.minute) { ForEach(0..<60) { minute in Text(verbatim: String(format: "%02d", minute)).tag(minute) .monospacedDigit() } } .labelsHidden() .minimalistPicker() .frame(alignment: .center) .fixedSize() } // AM/PM picker Picker("", selection: Binding( get: { isPM }, set: { newIsPM in if newIsPM && !isPM { // Switch to PM schedule.hour = schedule.hour == 0 ? 12 : schedule.hour + 12 } else if !newIsPM && isPM { // Switch to AM schedule.hour = schedule.hour == 12 ? 0 : schedule.hour - 12 } } )) { Text(verbatim: "AM").tag(false) Text(verbatim: "PM").tag(true) } .labelsHidden() .minimalistPicker() .frame(alignment: .center) .fixedSize() Spacer() // Save button (conditional, only in edit mode) if isInEditMode { Button { Task { do { try HomebrewAutoUpdateManager.shared.saveSchedule(schedule) } catch { await MainActor.run { errorMessage = error.localizedDescription showError = true } } } } label: { ZStack { Circle() .fill(Color.green.opacity(0.3)) .frame(width: 16, height: 16) Image(systemName: "checkmark.circle") .font(.system(size: 16, weight: .light)) .foregroundStyle(.green) } } .buttonStyle(.plain) .help("Save this schedule") } // Revert button (conditional, only for existing schedules in edit mode) if isInEditMode && !isNewSchedule { Button { HomebrewAutoUpdateManager.shared.revertSchedule(schedule) } label: { ZStack { Circle() .fill(Color.blue.opacity(0.3)) .frame(width: 16, height: 16) Image(systemName: "arrow.counterclockwise.circle") .font(.system(size: 16, weight: .light)) .foregroundStyle(.blue) } } .buttonStyle(.plain) .help("Revert to original schedule") } // Delete button Button { Task { do { try HomebrewAutoUpdateManager.shared.deleteSchedule(schedule) } catch { await MainActor.run { errorMessage = error.localizedDescription showError = true } } } } label: { ZStack { Circle() .fill(Color.red.opacity(0.3)) .frame(width: 16, height: 16) Image(systemName: "xmark.circle") .font(.system(size: 16, weight: .light)) .foregroundStyle(.red) } } .buttonStyle(.plain) .help("Delete this schedule") } .padding(.horizontal, 5) .padding(8) .background( RoundedRectangle(cornerRadius: 8) .fill(ThemeColors.shared(for: colorScheme).primaryBG) ) .padding(6) .overlay( RoundedRectangle(cornerRadius: 8) .strokeBorder( isInEditMode ? ThemeColors.shared(for: colorScheme).accent : Color.clear, style: StrokeStyle(lineWidth: 1.0, dash: [5, 3]) ) ) } .opacity(isDisabled ? 0.4 : 1.0) .disabled(isDisabled) .alert("Error", isPresented: $showError) { Button("Okay", role: .cancel) {} } message: { Text(errorMessage ?? "Unknown error") } } } ================================================ FILE: Pearcleaner/Views/Brew/HomebrewView.swift ================================================ // // HomebrewView.swift // Pearcleaner // // Created by Alin Lupascu on 10/01/25. // import SwiftUI import AlinFoundation enum HomebrewViewSection: String, CaseIterable { case browse = "Browse" case taps = "Taps" case autoUpdate = "Auto Update" case maintenance = "Maintenance" var icon: String { switch self { case .browse: return "magnifyingglass" case .taps: return "point.3.filled.connected.trianglepath.dotted" case .autoUpdate: return "clock.arrow.2.circlepath" case .maintenance: return "wrench.and.screwdriver.fill" } } } struct HomebrewView: View { @EnvironmentObject var brewManager: HomebrewManager @ObservedObject private var brewController = HomebrewController.shared @ObservedObject private var consoleManager = GlobalConsoleManager.shared @EnvironmentObject var appState: AppState @Environment(\.colorScheme) var colorScheme @State private var selectedSection: HomebrewViewSection = .browse @State private var isLoadingInitialData: Bool = false @State private var drawerOpen: Bool = false @State private var selectedPackage: HomebrewSearchResult? @State private var selectedPackageIsCask: Bool = false @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true var body: some View { VStack(spacing: 0) { if !HomebrewController.shared.isInstalled { // Show message when Homebrew is not installed VStack(spacing: 20) { Spacer() Image(systemName: "exclamationmark.triangle") .font(.system(size: 60)) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text("Homebrew Not Installed") .font(.title) .fontWeight(.bold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Text("Homebrew is not installed on your system. This feature requires Homebrew to manage packages.") .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .multilineTextAlignment(.center) .padding(.horizontal, 40) Button { if let url = URL(string: "https://brew.sh") { NSWorkspace.shared.open(url) } } label: { Text("Install Homebrew") .font(.callout) } .buttonStyle(.borderedProminent) Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) } else { // Normal Homebrew view content ZStack { VStack(spacing: 0) { // Section Picker HStack { Spacer() Picker("", selection: $selectedSection) { ForEach(HomebrewViewSection.allCases, id: \.self) { section in Label(section.rawValue, systemImage: section.icon) .tag(section) } } .labelsHidden() .pickerStyle(.segmented) .fixedSize() .disabled(isLoadingInitialData) .opacity(isLoadingInitialData ? 0.5 : 1.0) Spacer() } .padding(.horizontal, 20) .padding(.top, 10) .padding(.bottom, 5) // Section Content Group { switch selectedSection { case .browse: SearchInstallSection( onPackageSelected: { package, isCask in selectedPackage = package selectedPackageIsCask = isCask withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) { drawerOpen = true } } ) case .taps: TapManagementSection() case .autoUpdate: AutoUpdateSection() case .maintenance: MaintenanceSection() } } .transition(.opacity) .animation(animationEnabled ? .easeInOut(duration: 0.2) : .none, value: selectedSection) // Console View (inset at bottom of VStack) if consoleManager.showConsole { GlobalConsoleView( output: consoleManager.consoleOutput, height: $consoleManager.consoleHeight, onClear: { Task { @MainActor in consoleManager.clearOutput() } } ) .frame(height: consoleManager.consoleHeight) .transition(.move(edge: .bottom)) } } .frame(maxWidth: .infinity, maxHeight: .infinity) // Package Details Sidebar (overlays entire view) PackageDetailsSidebar( drawerOpen: $drawerOpen, package: selectedPackage, isCask: selectedPackageIsCask, onClose: { withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) { drawerOpen = false } } ) .padding([.trailing, .bottom], 20) } .animation( animationEnabled ? .spring(response: 0.35, dampingFraction: 0.8) : .none, value: drawerOpen) .animation( .spring(response: 0.4, dampingFraction: 0.8), value: consoleManager.showConsole) .task { // Load other data in parallel (installed packages loaded in SearchInstallSection) async let taps: Void = brewManager.loadTaps() async let version: Void = brewManager.loadBrewVersion() async let cache: Void = brewManager.loadCacheSize() async let analytics: Void = brewManager.checkAnalyticsStatus() _ = await (taps, version, cache, analytics) } } } .frame(maxWidth: .infinity, maxHeight: .infinity) .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("HomebrewViewShouldRefresh"))) { _ in Task { switch selectedSection { case .browse: await brewManager.loadInstalledPackages() await brewManager.loadAvailablePackages(appState: appState, forceRefresh: true) case .taps: await brewManager.loadTaps() case .autoUpdate: HomebrewAutoUpdateManager.shared.refreshState() case .maintenance: await brewManager.refreshMaintenance() } } } .toolbar { TahoeToolbarItem(placement: .navigation) { HStack { VStack(alignment: .leading) { Text("Homebrew Manager") .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .font(.title2) .fontWeight(.bold) Text("Manage Homebrew packages, taps, and maintenance") .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } } ToolbarItem { Spacer() } TahoeToolbarItem(isGroup: true) { Button { withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) { consoleManager.showConsole.toggle() } } label: { Label("Console", systemImage: consoleManager.showConsole ? "terminal.fill" : "terminal") } .help("Toggle console output") if brewController.isOperationRunning { Button { brewController.cancelOperation() } label: { Label("Stop", systemImage: "stop.circle") } .help("Cancel running Homebrew operation") } else { Button { Task { switch selectedSection { case .browse: await brewManager.loadInstalledPackages() await brewManager.loadAvailablePackages(appState: appState, forceRefresh: true) // Refresh sortedApps to pick up newly installed casks with proper size let folderPaths = await MainActor.run { FolderSettingsManager.shared.folderPaths } await loadAppsAsync(folderPaths: folderPaths, useStreaming: false) // Update categories to pick up latest app names from sortedApps and re-sort await MainActor.run { brewManager.updateInstalledCategories() } case .taps: await brewManager.loadTaps() case .autoUpdate: HomebrewAutoUpdateManager.shared.refreshState() case .maintenance: await brewManager.refreshMaintenance() } } } label: { Label("Refresh", systemImage: "arrow.counterclockwise") } .help("Refresh \(selectedSection.rawValue.lowercased()) data") } } } .onChange(of: consoleManager.showConsole) { newValue in Task { @MainActor in brewController.consoleEnabled = newValue } } } } ================================================ FILE: Pearcleaner/Views/Brew/LogViewerSheet.swift ================================================ // // LogViewerSheet.swift // Pearcleaner // // Created by Alin Lupascu on 10/26/25. // import SwiftUI struct LogViewerSheet: View { let logContent: String let onClose: () -> Void @Environment(\.colorScheme) var colorScheme var body: some View { StandardSheetView( title: "Homebrew Auto-Update Log", width: 700, height: 500, onClose: onClose ) { // Content ScrollView { Text(logContent) .font(.system(.body, design: .monospaced)) .textSelection(.enabled) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .frame(maxWidth: .infinity, alignment: .leading) } } actionButtons: { Button("Close") { onClose() } .keyboardShortcut(.cancelAction) } } } ================================================ FILE: Pearcleaner/Views/Brew/MaintenanceSection.swift ================================================ // // MaintenanceSection.swift // Pearcleaner // // Created by Alin Lupascu on 10/01/25. // import SwiftUI import AlinFoundation struct MaintenanceSection: View { @EnvironmentObject var brewManager: HomebrewManager @Environment(\.colorScheme) var colorScheme @State private var isUpdatingBrew: Bool = false @State private var isRunningDoctor: Bool = false @State private var isPurgingCache: Bool = false @State private var doctorOutput: String = "" @State private var showDoctorSheet: Bool = false @State private var doctorHealthy: Bool? = true // Optimistic: assume healthy, update if issues found @State private var isCheckingHealthOnAppear: Bool = false @State private var isCheckingVersionOnAppear: Bool = false @State private var isCheckingCacheSize: Bool = false @State private var refreshID = UUID() @AppStorage("settings.interface.scrollIndicators") private var scrollIndicators: Bool = false var body: some View { ScrollView { VStack(alignment: .leading, spacing: 20) { // Homebrew Version Section GroupBox { VStack(alignment: .leading, spacing: 12) { HStack { VStack(alignment: .leading, spacing: 4) { HStack(spacing: 6) { Text("Homebrew Version") .font(.headline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) if isCheckingVersionOnAppear { ProgressView() .controlSize(.small) .frame(width: 14, height: 14) } else if !brewManager.brewVersion.isEmpty { if brewManager.updateAvailable { Image(systemName: "arrow.up.circle.fill") .foregroundStyle(.orange) .font(.callout) .help("Homebrew update available") } else { Image(systemName: "checkmark") .foregroundStyle(.green) .font(.callout) .help("Homebrew is up to date") } } } if !brewManager.brewVersion.isEmpty { if brewManager.updateAvailable && !brewManager.latestBrewVersion.isEmpty { // Show current → latest when update available Text(verbatim: "\(brewManager.brewVersion) → \(brewManager.latestBrewVersion)") .font(.title3) .fontWeight(.semibold) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) } else { // Show just current version when up to date Text(verbatim: brewManager.brewVersion) .font(.title3) .fontWeight(.semibold) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) } } else { Text("Unknown") .font(.title3) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } Spacer() Button { Task { isUpdatingBrew = true do { try await HomebrewController.shared.updateBrew() await brewManager.checkForUpdate() } catch { printOS("Error updating Homebrew: \(error)") } isUpdatingBrew = false } } label: { if isUpdatingBrew { HStack(spacing: 8) { ProgressView() .scaleEffect(0.8) Text("Updating...") } } else { Label("Update Homebrew", systemImage: "arrow.triangle.2.circlepath") } } .buttonStyle(ControlGroupButtonStyle( foregroundColor: ThemeColors.shared(for: colorScheme).accent, shape: Capsule(style: .continuous), level: .primary, skipControlGroup: true )) .disabled(isUpdatingBrew) } Text("Update Homebrew to the latest version using brew update") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .padding() } .groupBoxStyle(TransparentGroupBox()) // Doctor Section GroupBox { VStack(alignment: .leading, spacing: 12) { HStack { VStack(alignment: .leading, spacing: 4) { HStack(spacing: 6) { Text("Homebrew Doctor") .font(.headline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) if let isHealthy = doctorHealthy { Button { if !isHealthy { showDoctorSheet = true } } label: { Image(systemName: isHealthy ? "checkmark" : "exclamationmark.octagon") .foregroundStyle(isHealthy ? .green : .red) .font(.callout) } .buttonStyle(.plain) .help(isHealthy ? "Your Homebrew installation is healthy" : "Click to view issues") } } Text("Check for potential issues") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } Spacer() Button { Task { isRunningDoctor = true do { doctorOutput = try await HomebrewController.shared.runDoctor() doctorHealthy = doctorOutput.contains("Your system is ready to brew") if !doctorHealthy! { showDoctorSheet = true } } catch { printOS("Error running doctor: \(error)") } isRunningDoctor = false } } label: { if isRunningDoctor { HStack(spacing: 8) { ProgressView() .scaleEffect(0.8) Text("Running...") } } else { Label("Run Doctor", systemImage: "stethoscope") } } .buttonStyle(ControlGroupButtonStyle( foregroundColor: ThemeColors.shared(for: colorScheme).accent, shape: Capsule(style: .continuous), level: .primary, skipControlGroup: true )) .disabled(isRunningDoctor) } } .padding() } .groupBoxStyle(TransparentGroupBox()) // Cleanup Section GroupBox { VStack(alignment: .leading, spacing: 12) { HStack { VStack(alignment: .leading, spacing: 4) { HStack(spacing: 6) { Text("Cleanup") .font(.headline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) if isCheckingCacheSize { ProgressView() .controlSize(.small) .frame(width: 14, height: 14) } } HStack(spacing: 6) { Text("Cache Size:") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) if isCheckingCacheSize { Text("Calculating...") .font(.caption) .fontWeight(.semibold) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } else { Text(ByteCountFormatter.string(fromByteCount: brewManager.cacheSize, countStyle: .file)) .font(.caption) .fontWeight(.semibold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } } } Spacer() Button { Task { isPurgingCache = true do { try await HomebrewController.shared.performFullCleanup() await brewManager.loadCacheSize() } catch { printOS("Error performing cleanup: \(error)") } isPurgingCache = false } } label: { if isPurgingCache { HStack(spacing: 8) { ProgressView() .scaleEffect(0.8) Text("Cleaning...") } } else { Label("Run Cleanup", systemImage: "trash") } } .buttonStyle(ControlGroupButtonStyle( foregroundColor: .red, shape: Capsule(style: .continuous), level: .primary, skipControlGroup: true )) .disabled(isPurgingCache || brewManager.cacheSize == 0) .opacity((isPurgingCache || brewManager.cacheSize == 0) ? 0.5 : 1.0) } Text("Removes old versions, orphaned dependencies, and all cache files including latest versions") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .padding() } .groupBoxStyle(TransparentGroupBox()) // Analytics Section GroupBox { VStack(alignment: .leading, spacing: 12) { HStack { VStack(alignment: .leading, spacing: 4) { Text("Analytics") .font(.headline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Text(brewManager.analyticsEnabled ? "Analytics are enabled" : "Analytics are disabled") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } Spacer() Toggle("", isOn: Binding( get: { brewManager.analyticsEnabled }, set: { newValue in // Update UI immediately for fluid experience brewManager.analyticsEnabled = newValue // Run command in background Task { do { try await HomebrewController.shared.setAnalyticsStatus(enabled: newValue) } catch { printOS("Error toggling analytics: \(error)") // Revert on error await MainActor.run { brewManager.analyticsEnabled = !newValue } } } } )) .toggleStyle(.switch) } Text("Homebrew collects anonymous analytics to help improve the project") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .padding() } .groupBoxStyle(TransparentGroupBox()) // Statistics Section GroupBox { VStack(alignment: .leading, spacing: 12) { Text("Statistics") .font(.headline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) HStack(spacing: 0) { // Formulae VStack(spacing: 4) { Text(verbatim: "\(brewManager.installedFormulae.count)") .font(.title2) .fontWeight(.bold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Text("Formulae") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .frame(maxWidth: .infinity) Divider() .frame(height: 40) // Casks VStack(spacing: 4) { Text(verbatim: "\(brewManager.installedCasks.count)") .font(.title2) .fontWeight(.bold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Text("Casks") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .frame(maxWidth: .infinity) Divider() .frame(height: 40) // Taps VStack(spacing: 4) { Text(verbatim: "\(brewManager.availableTaps.count)") .font(.title2) .fontWeight(.bold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Text("Taps") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .frame(maxWidth: .infinity) Divider() .frame(height: 40) // Outdated VStack(spacing: 4) { Text(verbatim: "\(brewManager.outdatedPackages.count)") .font(.title2) .fontWeight(.bold) .foregroundStyle(brewManager.outdatedPackages.isEmpty ? ThemeColors.shared(for: colorScheme).primaryText : .orange) Text("Outdated") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .frame(maxWidth: .infinity) } } .padding() } .groupBoxStyle(TransparentGroupBox()) } .padding(20) .padding(.bottom, 20) } .scrollIndicators(scrollIndicators ? .automatic : .never) .sheet(isPresented: $showDoctorSheet) { DoctorOutputSheet(output: doctorOutput, isPresented: $showDoctorSheet) } .onAppear { runAllChecks() } .onChange(of: brewManager.maintenanceRefreshTrigger) { _ in runAllChecks() } } private func runAllChecks() { // Run health check, version check, and cache size check in parallel if !isCheckingHealthOnAppear && !isCheckingVersionOnAppear && !isCheckingCacheSize { // Check cache size (instant ~5-20ms with Swift calculation) Task { isCheckingCacheSize = true await brewManager.loadCacheSize() isCheckingCacheSize = false } // Run both checks in parallel Task { isCheckingVersionOnAppear = true await brewManager.checkForUpdate() isCheckingVersionOnAppear = false } Task { isCheckingHealthOnAppear = true do { doctorOutput = try await HomebrewController.shared.runDoctor() doctorHealthy = doctorOutput.contains("Your system is ready to brew") } catch { printOS("Error running health check on appear: \(error)") doctorHealthy = false } isCheckingHealthOnAppear = false } } } } // MARK: - Doctor Output Sheet struct DoctorOutputSheet: View { let output: String @Binding var isPresented: Bool @Environment(\.colorScheme) var colorScheme var isHealthy: Bool { return output.contains("Your system is ready to brew") } var body: some View { StandardSheetView( title: "Homebrew Doctor", width: 700, height: 500, onClose: { isPresented = false } ) { // Content ScrollView { if isHealthy { VStack(spacing: 12) { Image(systemName: "checkmark.circle.fill") .font(.system(size: 48)) .foregroundStyle(.green) Text("Your Homebrew installation is healthy!") .font(.headline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Text("No issues detected") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .frame(maxWidth: .infinity, maxHeight: .infinity) } else { VStack(alignment: .leading, spacing: 12) { Text("Issues detected:") .font(.headline) .foregroundStyle(.red) Text(output) .font(.system(.caption, design: .monospaced)) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .textSelection(.enabled) .frame(maxWidth: .infinity, alignment: .leading) } .padding() .background( RoundedRectangle(cornerRadius: 8) .fill(ThemeColors.shared(for: colorScheme).secondaryBG) ) } } } actionButtons: { Button("Close") { isPresented = false } .buttonStyle(ControlGroupButtonStyle( foregroundColor: ThemeColors.shared(for: colorScheme).accent, shape: Capsule(style: .continuous), level: .primary, skipControlGroup: true )) } } } // MARK: - Transparent GroupBox Style struct TransparentGroupBox: GroupBoxStyle { func makeBody(configuration: Configuration) -> some View { VStack(alignment: .leading) { configuration.content } } } ================================================ FILE: Pearcleaner/Views/Brew/PackageDetailsSidebar.swift ================================================ // // PackageDetailsSidebar.swift // Pearcleaner // // Created by Alin Lupascu on 10/07/25. // import SwiftUI import AlinFoundation struct PackageDetailsSidebar: View { @Binding var drawerOpen: Bool let package: HomebrewSearchResult? let isCask: Bool let onClose: () -> Void var body: some View { if drawerOpen, let package = package { HStack(spacing: 0) { Spacer() PackageDetailsDrawer( package: package, isCask: isCask, onClose: onClose ) // .padding() .frame(width: 300) .ifGlassSidebar() } .background(.black.opacity(0.00000000001)) .transition(.move(edge: .trailing)) .onTapGesture { onClose() } } } } ================================================ FILE: Pearcleaner/Views/Brew/SearchInstallSection.swift ================================================ // // SearchInstallSection.swift // Pearcleaner // // Created by Alin Lupascu on 10/01/25. // import SwiftUI import AlinFoundation enum HomebrewSearchType: String, CaseIterable { case installed = "Installed" case available = "Available" } enum BrewPackageSortOption: String, CaseIterable { case name = "Name" case size = "Size" var systemImage: String { switch self { case .name: return "list.bullet" case .size: return "arrow.up.arrow.down" } } } struct SearchInstallSection: View { let onPackageSelected: (HomebrewSearchResult, Bool) -> Void @EnvironmentObject var brewManager: HomebrewManager @EnvironmentObject var appState: AppState @Environment(\.colorScheme) var colorScheme @State private var searchQuery: String = "" @State private var searchType: HomebrewSearchType = .installed @State private var installedCollapsedCategories: Set = [] @State private var availableCollapsedCategories: Set = [] @State private var updatingPackages: Set = [] @State private var isUpdatingAll: Bool = false @AppStorage("settings.interface.scrollIndicators") private var scrollIndicators: Bool = false @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true @AppStorage("settings.brew.showOnlyInstalledOnRequest") private var showOnlyInstalledOnRequest: Bool = false @State private var formulaeSortOption: BrewPackageSortOption = .name @State private var casksSortOption: BrewPackageSortOption = .name /// Helper to match search query against package name, displayName, and description private func matchesSearchQuery(_ package: HomebrewSearchResult, query: String) -> Bool { // Search in name (brew ID) if package.name.localizedCaseInsensitiveContains(query) { return true } // Search in displayName (human-readable app name) if let displayName = package.displayName, displayName.localizedCaseInsensitiveContains(query) { return true } // Search in description if let description = package.description, description.localizedCaseInsensitiveContains(query) { return true } return false } private var displayedResults: [HomebrewSearchResult] { if searchType == .available { // Flat list for Available tab - all packages combined let allPackages = brewManager.allAvailableFormulae + brewManager.allAvailableCasks if searchQuery.isEmpty { return allPackages } else { return allPackages.filter { matchesSearchQuery($0, query: searchQuery) } } } else { // Installed uses categorized view return [] } } private func updateAllOutdated(packages: [HomebrewSearchResult]) { Task { @MainActor in isUpdatingAll = true defer { isUpdatingAll = false } var updatedPackageNames: [String] = [] for package in packages { printOS("Starting update for: \(package.name)") updatingPackages.insert(package.name) do { try await HomebrewController.shared.upgradePackage(name: package.name) printOS("Successfully updated: \(package.name)") updatedPackageNames.append(package.name) // Remove from outdated map and category immediately let shortName = package.name.components(separatedBy: "/").last ?? package.name brewManager.outdatedPackagesMap.removeValue(forKey: package.name) brewManager.outdatedPackagesMap.removeValue(forKey: shortName) brewManager.installedByCategory[.outdated]?.removeAll { $0.name == package.name || $0.name == shortName } } catch { printOS("Error updating package \(package.name): \(error)") } updatingPackages.remove(package.name) printOS("Finished update for: \(package.name)") } printOS("All updates complete. Refreshing updated packages...") // Targeted refresh - only update the packages that were upgraded (much faster than full scan) await brewManager.refreshSpecificPackages(updatedPackageNames) } } private func toggleCategoryCollapse(for category: String, tab: HomebrewSearchType) { if tab == .installed { if installedCollapsedCategories.contains(category) { installedCollapsedCategories.remove(category) } else { installedCollapsedCategories.insert(category) } } else { if availableCollapsedCategories.contains(category) { availableCollapsedCategories.remove(category) } else { availableCollapsedCategories.insert(category) } } } @ViewBuilder private var searchBar: some View { HStack(spacing: 12) { // Search field HStack { Image(systemName: "magnifyingglass") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) TextField("Search...", text: $searchQuery) .textFieldStyle(.plain) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) if !searchQuery.isEmpty { Button { searchQuery = "" } label: { Image(systemName: "xmark.circle.fill") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .buttonStyle(.plain) } } .padding(.horizontal, 12) .padding(.vertical, 8) .controlGroup(Capsule(style: .continuous), level: .primary) // Search type picker Picker("", selection: $searchType) { ForEach(HomebrewSearchType.allCases, id: \.self) { type in Text(type.rawValue).tag(type) } } .pickerStyle(.segmented) .labelsHidden() .fixedSize() .onChange(of: searchType) { _ in // Clear search when switching tabs searchQuery = "" } } .padding(.horizontal, 20) .padding(.top, 10) } private var resultsCountBar: some View { HStack { let totalCount = searchType == .installed ? (brewManager.installedFormulae.count + brewManager.installedCasks.count) : (brewManager.allAvailableFormulae.count + brewManager.allAvailableCasks.count) Text("\(totalCount) package\(totalCount == 1 ? "" : "s")") .font(.caption) .monospacedDigit() .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) // Show outdated count for Installed tab if searchType == .installed { let outdatedCount = brewManager.outdatedPackagesMap.count if outdatedCount > 0 { Text(verbatim: "|") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text("\(outdatedCount) outdated") .font(.caption) .monospacedDigit() .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } if (searchType == .installed && brewManager.isLoadingPackages) || (searchType == .available && brewManager.isLoadingAvailablePackages) { Text("Loading...") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } Spacer() } .padding(.horizontal, 20) .padding(.vertical, 10) } @ViewBuilder private var listContent: some View { GeometryReader { geometry in // Results List (full width) VStack(alignment: .leading, spacing: 0) { // Results or loading state if ((searchType == .installed && brewManager.installedFormulae.isEmpty && brewManager.installedCasks.isEmpty) || (searchType == .available && displayedResults.isEmpty)) && !searchQuery.isEmpty { VStack(alignment: .center) { Spacer() Image(systemName: "exclamationmark.magnifyingglass") .font(.system(size: 50)) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text("No results found") .font(.title2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text("Try a different search term") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() } .frame(maxWidth: .infinity) } else { ScrollView { LazyVStack(spacing: 8) { if searchType == .installed { // Show categorized view for installed packages (matches Updater view pattern) // Outdated category let outdatedPackages = brewManager.installedByCategory[.outdated] ?? [] let filteredOutdated = searchQuery.isEmpty ? outdatedPackages : outdatedPackages.filter { matchesSearchQuery($0, query: searchQuery) } // Calculate which category is actually first (visible) let isOutdatedVisible = !filteredOutdated.isEmpty if isOutdatedVisible { InstalledCategoryView( category: .outdated, packages: filteredOutdated, isLoading: brewManager.isLoadingOutdated, collapsed: installedCollapsedCategories.contains("Outdated"), onToggle: { guard !filteredOutdated.isEmpty && !brewManager.isLoadingOutdated else { return } withAnimation(.easeInOut(duration: animationEnabled ? 0.3 : 0)) { toggleCategoryCollapse(for: "Outdated", tab: .installed) } }, isFirst: true, onPackageSelected: onPackageSelected, updatingPackages: updatingPackages, brewManager: brewManager, onUpdateAll: filteredOutdated.count > 1 ? { updateAllOutdated(packages: filteredOutdated) } : nil, colorScheme: colorScheme, showOnlyInstalledOnRequest: $showOnlyInstalledOnRequest, sortOption: $formulaeSortOption ) } // Formulae category let formulaePackages = brewManager.installedByCategory[.formulae] ?? [] let searchFiltered = searchQuery.isEmpty ? formulaePackages : formulaePackages.filter { matchesSearchQuery($0, query: searchQuery) } let filteredFormulae = showOnlyInstalledOnRequest ? searchFiltered.filter { package in brewManager.installedFormulae.first(where: { $0.name == package.name })?.installedOnRequest == true } : searchFiltered InstalledCategoryView( category: .formulae, packages: filteredFormulae, isLoading: brewManager.isLoadingPackages, collapsed: installedCollapsedCategories.contains("Formulae"), onToggle: { guard !filteredFormulae.isEmpty && !brewManager.isLoadingPackages else { return } withAnimation(.easeInOut(duration: animationEnabled ? 0.3 : 0)) { toggleCategoryCollapse(for: "Formulae", tab: .installed) } }, isFirst: !isOutdatedVisible, onPackageSelected: onPackageSelected, updatingPackages: updatingPackages, brewManager: brewManager, onUpdateAll: nil, colorScheme: colorScheme, showOnlyInstalledOnRequest: $showOnlyInstalledOnRequest, sortOption: $formulaeSortOption ) // Casks category let casksPackages = brewManager.installedByCategory[.casks] ?? [] let filteredCasks = searchQuery.isEmpty ? casksPackages : casksPackages.filter { matchesSearchQuery($0, query: searchQuery) } InstalledCategoryView( category: .casks, packages: filteredCasks, isLoading: brewManager.isLoadingPackages, collapsed: installedCollapsedCategories.contains("Casks"), onToggle: { guard !filteredCasks.isEmpty && !brewManager.isLoadingPackages else { return } withAnimation(.easeInOut(duration: animationEnabled ? 0.3 : 0)) { toggleCategoryCollapse(for: "Casks", tab: .installed) } }, isFirst: false, onPackageSelected: onPackageSelected, updatingPackages: updatingPackages, brewManager: brewManager, onUpdateAll: nil, colorScheme: colorScheme, showOnlyInstalledOnRequest: $showOnlyInstalledOnRequest, sortOption: $casksSortOption ) } else { // Show categorized view for Available tab (matches Installed/Updater view pattern) // Formulae category let formulaePackages = brewManager.availableByCategory[.formulae] ?? [] let filteredFormulae = searchQuery.isEmpty ? formulaePackages : formulaePackages.filter { matchesSearchQuery($0, query: searchQuery) } if !filteredFormulae.isEmpty { AvailableCategoryView( category: .formulae, packages: filteredFormulae, collapsed: availableCollapsedCategories.contains("Formulae"), onToggle: { withAnimation(.easeInOut(duration: animationEnabled ? 0.3 : 0)) { toggleCategoryCollapse(for: "Formulae", tab: .available) } }, isFirst: true, onPackageSelected: onPackageSelected, updatingPackages: updatingPackages, brewManager: brewManager, colorScheme: colorScheme ) } // Casks category let casksPackages = brewManager.availableByCategory[.casks] ?? [] let filteredCasks = searchQuery.isEmpty ? casksPackages : casksPackages.filter { matchesSearchQuery($0, query: searchQuery) } if !filteredCasks.isEmpty { AvailableCategoryView( category: .casks, packages: filteredCasks, collapsed: availableCollapsedCategories.contains("Casks"), onToggle: { withAnimation(.easeInOut(duration: animationEnabled ? 0.3 : 0)) { toggleCategoryCollapse(for: "Casks", tab: .available) } }, isFirst: false, onPackageSelected: onPackageSelected, updatingPackages: updatingPackages, brewManager: brewManager, colorScheme: colorScheme ) } } } .padding(.horizontal, 20) .padding(.top, 10) .padding(.bottom, 20) } .id(searchType) // Give each tab its own scroll identity .scrollIndicators(scrollIndicators ? .automatic : .never) } } .frame(maxWidth: .infinity, maxHeight: .infinity) } .onAppear { Task { // Start loading installed packages immediately let loadInstalledTask = Task { if !brewManager.hasLoadedInstalledPackages { await brewManager.loadInstalledPackages() } } // Debounce JWS loading by 0.5s to let installed scan run uncontested first let loadAvailableTask = Task { try? await Task.sleep(nanoseconds: 500_000_000) // 0.5 second delay if !brewManager.hasLoadedAvailablePackages { await brewManager.loadAvailablePackages(appState: appState, forceRefresh: false) } } // Wait for both to complete _ = await loadInstalledTask.value _ = await loadAvailableTask.value // Re-enrich installed packages now that JWS data is available // This is very fast (~10-50ms) - re-runs updateInstalledCategories() with new JWS data await MainActor.run { brewManager.updateInstalledCategories() } } } } var body: some View { VStack(alignment: .leading, spacing: 0) { searchBar resultsCountBar listContent } } } // MARK: - Search Result Row View struct SearchResultRowView: View { let result: HomebrewSearchResult let isCask: Bool let onInfoTapped: () -> Void let updatingPackages: Set @EnvironmentObject var brewManager: HomebrewManager @Environment(\.colorScheme) var colorScheme @AppStorage("settings.general.confirmAlert") private var confirmAlert: Bool = false @State private var isHovered: Bool = false @State private var isInstalling: Bool = false @State private var isUninstalling: Bool = false @State private var showInstallAlert: Bool = false @State private var showUpdateAlert: Bool = false @State private var showUninstallAlert: Bool = false @State private var localPinState: Bool? // Local state for immediate UI update private var isAlreadyInstalled: Bool { // Extract short name from full name (e.g., "mhaeuser/mhaeuser/battery-toolkit" -> "battery-toolkit") let shortName = result.name.components(separatedBy: "/").last ?? result.name if isCask { return brewManager.installedCasks.contains { installedPackage in installedPackage.name == result.name || installedPackage.name == shortName } } else { return brewManager.installedFormulae.contains { installedPackage in installedPackage.name == result.name || installedPackage.name == shortName } } } private var isOutdated: Bool { guard isAlreadyInstalled else { return false } let shortName = result.name.components(separatedBy: "/").last ?? result.name // Check if package is in the outdated set from brew outdated return brewManager.outdatedPackagesMap.keys.contains(result.name) || brewManager.outdatedPackagesMap.keys.contains(shortName) } private var isPinned: Bool { guard !isCask && isAlreadyInstalled else { return false } // Use local state if available (for immediate UI update) if let localState = localPinState { return localState } let shortName = result.name.components(separatedBy: "/").last ?? result.name // Check if package is pinned in installed formulae return brewManager.installedFormulae.first(where: { $0.name == result.name || $0.name == shortName })?.isPinned ?? false } private var appIcon: NSImage? { guard isCask else { return nil } let shortName = result.name.components(separatedBy: "/").last ?? result.name // Try 1: Find matching app in AppState.shared.sortedApps by cask name (preferred) if let app = AppState.shared.sortedApps.first(where: { appInfo in appInfo.cask == result.name || appInfo.cask == shortName }) { return app.appIcon } // Try 2: Fallback - manually fetch icon from likely bundle path // This handles newly installed casks that haven't been added to sortedApps yet let capitalizedName = shortName.prefix(1).uppercased() + shortName.dropFirst() let possiblePaths = [ URL(fileURLWithPath: "/Applications/\(capitalizedName).app"), URL(fileURLWithPath: "/Applications/\(shortName).app"), URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent("Applications/\(capitalizedName).app"), URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent("Applications/\(shortName).app") ] for path in possiblePaths where FileManager.default.fileExists(atPath: path.path) { if let icon = AppInfoUtils.fetchAppIcon(for: path, wrapped: false) { return icon } } return nil } private var displayedName: String { // For installed casks, prefer the actual app name over the cask display name if isCask && isAlreadyInstalled { let shortName = result.name.components(separatedBy: "/").last ?? result.name // Find matching app in AppState.shared.sortedApps by cask name if let app = AppState.shared.sortedApps.first(where: { appInfo in appInfo.cask == result.name || appInfo.cask == shortName }) { return app.appName // Use the actual app name (e.g., "Yandex Disk") } } // Fallback to cask display name or name return result.displayName ?? result.name } private func getPackageSize() -> String? { let shortName = result.name.components(separatedBy: "/").last ?? result.name if isCask { return brewManager.installedCasks.first(where: { $0.name == result.name || $0.name == shortName })?.size } else { return brewManager.installedFormulae.first(where: { $0.name == result.name || $0.name == shortName })?.size } } private func calculateSize() async { let shortName = result.name.components(separatedBy: "/").last ?? result.name if isCask { // Calculate cask size from AppState.sortedApps let (sizeBytes, sizeFormatted) = await HomebrewController.shared.calculateCaskSize(name: result.name) await MainActor.run { if let index = brewManager.installedCasks.firstIndex(where: { $0.name == result.name || $0.name == shortName }) { brewManager.installedCasks[index].size = sizeFormatted brewManager.installedCasks[index].sizeBytes = sizeBytes } } } else { // Calculate formula size from Cellar directory guard let version = result.version else { return } let (sizeBytes, sizeFormatted) = await HomebrewController.shared.calculateFormulaSize(name: result.name, version: version) await MainActor.run { if let index = brewManager.installedFormulae.firstIndex(where: { $0.name == result.name || $0.name == shortName }) { brewManager.installedFormulae[index].size = sizeFormatted brewManager.installedFormulae[index].sizeBytes = sizeBytes } } } } @ViewBuilder private var actionButtons: some View { if isInstalling || updatingPackages.contains(result.name) { HStack(spacing: 6) { ProgressView() .scaleEffect(0.8) Text("Installing...") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } else if isUninstalling { HStack(spacing: 6) { ProgressView() .scaleEffect(0.8) Text("Uninstalling...") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } else if isOutdated { HStack(spacing: 8) { Button(isPinned ? "Update (Pinned)" : "Update") { if confirmAlert { showUpdateAlert = true } else { performUpdate() } } .buttonStyle(.plain) .foregroundStyle(.orange) Button("Uninstall") { if confirmAlert { showUninstallAlert = true } else { performUninstall() } } .buttonStyle(.plain) .foregroundStyle(.red) } .frame(alignment: .trailing) } else if isAlreadyInstalled { Button("Uninstall") { if confirmAlert { showUninstallAlert = true } else { performUninstall() } } .buttonStyle(.plain) .foregroundStyle(.red) } else { Button("Install") { if confirmAlert { showInstallAlert = true } else { performInstall() } } .buttonStyle(.plain) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) } } @ViewBuilder private var secondaryActionButtons: some View { // Tap indicator (custom taps only) if let tap = result.tap, tap != "homebrew/core" && tap != "homebrew/cask" { Image(systemName: "spigot") .font(.system(size: 14)) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .help("Installed from tap: \(tap)") } // Pin button (formulae only, and only if installed) if !isCask && isAlreadyInstalled { Button { // Capture current state before toggling let currentlyPinned = isPinned // Toggle local state immediately for instant UI feedback localPinState = !currentlyPinned // Perform actual pin/unpin in background Task { do { if currentlyPinned { try await HomebrewController.shared.unpinPackage(name: result.name) } else { try await HomebrewController.shared.pinPackage(name: result.name) } // Update the installed formula's isPinned status await MainActor.run { let shortName = result.name.components(separatedBy: "/").last ?? result.name if let index = brewManager.installedFormulae.firstIndex(where: { $0.name == result.name || $0.name == shortName }) { var updatedFormula = brewManager.installedFormulae[index] updatedFormula.isPinned = !currentlyPinned brewManager.installedFormulae[index] = updatedFormula } } } catch { printOS("Failed to toggle pin: \(error)") // Revert local state on error localPinState = currentlyPinned } } } label: { Image(systemName: isPinned ? "pin.fill" : "pin") .font(.system(size: 14)) .foregroundStyle(isPinned ? .orange : ThemeColors.shared(for: colorScheme).secondaryText) } .buttonStyle(.plain) .help(isPinned ? "Unpin version" : "Pin version") } // Info button (hide for tap packages - no API available) if result.tap == nil || result.tap == "homebrew/core" || result.tap == "homebrew/cask" { Button { onInfoTapped() } label: { Image(systemName: "info.circle") .font(.system(size: 16)) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) } .buttonStyle(.plain) } } var body: some View { HStack(alignment: .center, spacing: 12) { // Package icon if let icon = appIcon { // Show actual app icon for casks Image(nsImage: icon) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 40, height: 40) } else { // Fallback to SF Symbol ZStack { Circle() .fill((isCask ? Color.purple : Color.green).opacity(0.2)) .frame(width: 40, height: 40) Image(systemName: isCask ? "macwindow" : "terminal") .font(.system(size: 18, weight: .medium)) .foregroundStyle(isCask ? .purple : .green) } } // Package name and description VStack(alignment: .leading, spacing: 4) { HStack(spacing: 4) { Text(displayedName) .font(.headline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) // Show version if available if let version = result.version { if isAlreadyInstalled { // Installed package - check if outdated and show update arrow if let versions = brewManager.getOutdatedVersions(for: result.name) { // Match Updater view color scheme: orange (outdated) → green (new) (Text(verbatim: "(") .foregroundColor(ThemeColors.shared(for: colorScheme).secondaryText) + Text(verbatim: versions.installed) .foregroundColor(.orange) + Text(verbatim: " → ") .foregroundColor(ThemeColors.shared(for: colorScheme).secondaryText) + Text(verbatim: versions.available) .foregroundColor(.green) + Text(verbatim: ")") .foregroundColor(ThemeColors.shared(for: colorScheme).secondaryText)) .font(.footnote) } else { Text(verbatim: "(\(version.stripBrewRevisionSuffix()))") .font(.footnote) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } else { // Not installed - show available version Text(verbatim: "(\(version.stripBrewRevisionSuffix()))") .font(.footnote) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText.opacity(0.8)) } } } if let description = result.description { Text(description) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .lineLimit(2) } } Spacer() // Package size (for installed packages) if isAlreadyInstalled { if let size = getPackageSize() { Text(size) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } else { Text("Calculating...") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .task(id: result.name) { await calculateSize() } } } // Install/Uninstall/Update action buttons actionButtons // Tap indicator, Pin and Info buttons secondaryActionButtons } .padding() .background( RoundedRectangle(cornerRadius: 8) .fill(isHovered ? ThemeColors.shared(for: colorScheme).secondaryBG.opacity(0.8) : ThemeColors.shared(for: colorScheme).secondaryBG ) ) .onHover { hovering in withAnimation(.easeInOut(duration: 0.2)) { isHovered = hovering } } .onAppear { // Fallback: If package is installed but has 0 size, calculate directly from disk // Note: Casks are handled by .task modifier via calculateSize() → calculateCaskSize() // which correctly gets the app bundle size from AppInfo guard isAlreadyInstalled else { return } let shortName = result.name.components(separatedBy: "/").last ?? result.name let sizeBytes: Int64? if isCask { sizeBytes = brewManager.installedCasks.first(where: { $0.name == result.name || $0.name == shortName })?.sizeBytes } else { sizeBytes = brewManager.installedFormulae.first(where: { $0.name == result.name || $0.name == shortName })?.sizeBytes } // If size is nil or 0, calculate it directly from disk if sizeBytes == nil || sizeBytes == 0 { Task { // For both casks and formulae, use the existing calculation logic // Casks: calculateSize() → HomebrewController.calculateCaskSize() uses AppInfo // Formulae: calculateSize() → HomebrewController.calculateFormulaSize() uses Cellar await calculateSize() } } } .alert("Install \(result.name)?", isPresented: $showInstallAlert) { Button("Cancel", role: .cancel) { } Button("Install") { Task { await MainActor.run { isInstalling = true } defer { Task { @MainActor in isInstalling = false } } do { try await HomebrewController.shared.installPackage(name: result.name, cask: isCask) // Targeted refresh - only update this newly installed package (much faster than full scan) await brewManager.refreshSpecificPackages([result.name]) // Refresh AppState.sortedApps to include newly installed GUI app (casks only) if isCask { await loadAndAddCaskApp(caskName: result.name) } } catch let error as HomebrewError { handleBrewInstallError(error, packageName: result.name, isCask: isCask, brewManager: brewManager) } catch { printOS("Error installing package \(result.name): \(error)") } } } } message: { Text("This will install \(result.displayName ?? result.name) using Homebrew. This may take several minutes.") } .alert("Update \(result.displayName ?? result.name)?", isPresented: $showUpdateAlert) { Button("Cancel", role: .cancel) { } Button("Update") { Task { await MainActor.run { isInstalling = true } defer { Task { @MainActor in isInstalling = false } } do { try await HomebrewController.shared.upgradePackage(name: result.name) // Targeted refresh - only update this specific package (much faster than full scan) await brewManager.refreshSpecificPackages([result.name]) // Refresh AppState.sortedApps to reflect updated version (casks only) if isCask { // Flush bundle cache for the app before reloading (version changed) if let matchingApp = findAppByCask(result.name) { flushBundleCaches(for: [matchingApp]) } await loadAndUpdateCaskApp(caskName: result.name) } } catch { printOS("Error updating package \(result.name): \(error)") } } } } message: { Text("This will upgrade \(result.displayName ?? result.name) to the latest version using Homebrew. This may take several minutes.") } .alert("Uninstall \(result.displayName ?? result.name)?", isPresented: $showUninstallAlert) { Button("Cancel", role: .cancel) { } Button("Uninstall", role: .destructive) { Task { await MainActor.run { isUninstalling = true } defer { Task { @MainActor in isUninstalling = false } } do { try await HomebrewUninstaller.shared.uninstallPackage(name: result.name, cask: isCask, zap: true) // Remove from installed lists instead of full refresh let shortName = result.name.components(separatedBy: "/").last ?? result.name if isCask { await MainActor.run { brewManager.installedCasks.removeAll { $0.name == result.name || $0.name == shortName } } // Refresh AppState.sortedApps to remove uninstalled app (casks only) let folderPaths = await MainActor.run { FolderSettingsManager.shared.folderPaths } // Optimized: Only flush bundle for the uninstalled app (if still exists) if let matchingApp = findAppByCask(result.name) { flushBundleCaches(for: [matchingApp]) } else { flushBundleCaches(for: AppState.shared.sortedApps) // Fallback } invalidateCaskLookupCache() await loadAppsAsync(folderPaths: folderPaths, useStreaming: false) } else { await MainActor.run { brewManager.installedFormulae.removeAll { $0.name == result.name || $0.name == shortName } } } await MainActor.run { brewManager.outdatedPackagesMap.removeValue(forKey: result.name) brewManager.outdatedPackagesMap.removeValue(forKey: shortName) } // Refresh categorized view to update UI (for both casks and formulae) await MainActor.run { brewManager.updateInstalledCategories() } } catch let error as HomebrewError { handleBrewUninstallError(error, packageName: result.name, brewManager: brewManager) } catch { printOS("Error uninstalling package \(result.name): \(error)") } } } } message: { Text("This will completely uninstall \(result.displayName ?? result.name) and remove all associated files. This action cannot be undone.") } } // MARK: - Helper Functions private func performInstall() { Task { await MainActor.run { isInstalling = true } defer { Task { @MainActor in isInstalling = false } } do { try await HomebrewController.shared.installPackage(name: result.name, cask: isCask) // Targeted refresh - only update this newly installed package (much faster than full scan) await brewManager.refreshSpecificPackages([result.name]) // Refresh AppState.sortedApps to include newly installed GUI app (casks only) if isCask { await loadAndAddCaskApp(caskName: result.name) } } catch { printOS("Error installing package \(result.name): \(error)") } } } private func performUpdate() { Task { await MainActor.run { isInstalling = true } defer { Task { @MainActor in isInstalling = false } } do { // Check if formula is pinned before updating let wasPinned = isPinned // If pinned, unpin before updating if wasPinned { try await HomebrewController.shared.unpinPackage(name: result.name) } // Perform the update var updateSucceeded = false do { try await HomebrewController.shared.upgradePackage(name: result.name) updateSucceeded = true } catch { // Re-pin if update failed and package was originally pinned if wasPinned { try? await HomebrewController.shared.pinPackage(name: result.name) } throw error // Re-throw to be caught by outer catch } // Re-pin at new version if update succeeded and was originally pinned if wasPinned && updateSucceeded { try await HomebrewController.shared.pinPackage(name: result.name) } // Targeted refresh - only update this specific package (much faster than full scan) await brewManager.refreshSpecificPackages([result.name]) // Refresh AppState.sortedApps to reflect updated version (casks only) if isCask { // Flush bundle cache for the app before reloading (version changed) if let matchingApp = findAppByCask(result.name) { flushBundleCaches(for: [matchingApp]) } await loadAndUpdateCaskApp(caskName: result.name) } } catch { printOS("Error updating package \(result.name): \(error)") } } } private func performUninstall() { Task { await MainActor.run { isUninstalling = true } defer { Task { @MainActor in isUninstalling = false } } do { try await HomebrewUninstaller.shared.uninstallPackage(name: result.name, cask: isCask, zap: true) // Remove from installed lists instead of full refresh let shortName = result.name.components(separatedBy: "/").last ?? result.name if isCask { await MainActor.run { brewManager.installedCasks.removeAll { $0.name == result.name || $0.name == shortName } } // Refresh AppState.sortedApps to remove uninstalled app (casks only) let folderPaths = await MainActor.run { FolderSettingsManager.shared.folderPaths } // Optimized: Only flush bundle for the uninstalled app (if still exists) if let matchingApp = findAppByCask(result.name) { flushBundleCaches(for: [matchingApp]) } else { flushBundleCaches(for: AppState.shared.sortedApps) // Fallback } invalidateCaskLookupCache() await loadAppsAsync(folderPaths: folderPaths, useStreaming: false) } else { await MainActor.run { brewManager.installedFormulae.removeAll { $0.name == result.name || $0.name == shortName } } } await MainActor.run { brewManager.outdatedPackagesMap.removeValue(forKey: result.name) brewManager.outdatedPackagesMap.removeValue(forKey: shortName) } // Refresh categorized view to update UI (for both casks and formulae) await MainActor.run { brewManager.updateInstalledCategories() } } catch let error as HomebrewError { handleBrewUninstallError(error, packageName: result.name, brewManager: brewManager) } catch { printOS("Error uninstalling package \(result.name): \(error)") } } } } // MARK: - Package Details Drawer struct PackageDetailsDrawer: View { let package: HomebrewSearchResult let isCask: Bool let onClose: () -> Void @EnvironmentObject var brewManager: HomebrewManager @Environment(\.colorScheme) var colorScheme @State private var analytics: HomebrewAnalytics? @State private var isLoadingAnalytics: Bool = false @State private var isLoadingFullPackageInfo: Bool = false @State private var isInstalling: Bool = false @State private var showInstallAlert: Bool = false @State private var packageDetails: PackageDetailsType? // Type-safe package details private var isAlreadyInstalled: Bool { let shortName = package.name.components(separatedBy: "/").last ?? package.name if isCask { return brewManager.installedCasks.contains { $0.name == package.name || $0.name == shortName } } else { return brewManager.installedFormulae.contains { $0.name == package.name || $0.name == shortName } } } // Check if package data is incomplete (from installed package conversion) private var needsFullData: Bool { return package.license == nil && package.dependencies == nil && package.caveats == nil } var body: some View { VStack(spacing: 0) { // Content - Show loading OR details if isLoadingAnalytics || isLoadingFullPackageInfo { VStack(spacing: 12) { ProgressView() .scaleEffect(1.2) Text("Loading package details...") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .frame(maxWidth: .infinity, maxHeight: .infinity) } else if let packageDetails = packageDetails { // Render type-safe package details switch packageDetails { case .formula(let formula): FormulaDetailsView( formula: formula, analytics: analytics, isInstalling: $isInstalling, isAlreadyInstalled: isAlreadyInstalled, showInstallAlert: $showInstallAlert, brewManager: brewManager, colorScheme: colorScheme ) case .cask(let cask): CaskDetailsView( cask: cask, analytics: analytics, isInstalling: $isInstalling, isAlreadyInstalled: isAlreadyInstalled, showInstallAlert: $showInstallAlert, brewManager: brewManager, colorScheme: colorScheme ) } } } .frame(maxWidth: .infinity, maxHeight: .infinity) .onAppear { loadFullPackageInfoIfNeeded() loadAnalytics() loadInstalledPackagesIfNeeded() } } private func loadFullPackageInfoIfNeeded() { Task { isLoadingFullPackageInfo = true do { // Extract short name for matching (e.g., "homebrew/core/node" -> "node") let shortName = package.name.components(separatedBy: "/").last ?? package.name // Fetch type-safe package details from Homebrew API packageDetails = try await HomebrewController.shared.getPackageDetailsTyped( name: shortName, cask: isCask ) } catch { printOS("Failed to fetch package info for \(package.name): \(error)") } isLoadingFullPackageInfo = false } } private func loadAnalytics() { // Skip analytics for third-party tap packages (only available for homebrew/core and homebrew/cask) // If tap is nil, assume it's from the default tap (homebrew/core for formulae, homebrew/cask for casks) let tap = package.tap ?? (isCask ? "homebrew/cask" : "homebrew/core") guard tap == "homebrew/core" || tap == "homebrew/cask" else { return } Task { isLoadingAnalytics = true do { analytics = try await HomebrewController.shared.getAnalytics( name: package.name, cask: isCask ) } catch { printOS("Failed to fetch analytics: \(error)") } isLoadingAnalytics = false } } private func loadInstalledPackagesIfNeeded() { // If installed packages haven't been loaded yet, load them for dependency checking if brewManager.installedFormulae.isEmpty && brewManager.installedCasks.isEmpty { Task { await brewManager.loadInstalledPackages() } } } } // MARK: - Formula Details View struct FormulaDetailsView: View { let formula: FormulaDetails let analytics: HomebrewAnalytics? @Binding var isInstalling: Bool let isAlreadyInstalled: Bool @Binding var showInstallAlert: Bool let brewManager: HomebrewManager let colorScheme: ColorScheme @AppStorage("settings.interface.scrollIndicators") private var scrollIndicators: Bool = false @State private var fileCount: Int? @State private var totalSize: Int64? var body: some View { ScrollView(.vertical, showsIndicators: scrollIndicators) { VStack(alignment: .leading, spacing: 16) { // Package name and version VStack(alignment: .leading, spacing: 4) { Text(formula.name) .font(.title2) .fontWeight(.bold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) if let version = formula.version { HStack(spacing: 4) { Text(verbatim: "v\(version)") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) // Show pinned icon if this formula is pinned if let installedFormula = brewManager.installedFormulae.first(where: { $0.name == formula.name || $0.name == formula.name.components(separatedBy: "/").last }), installedFormula.isPinned { Image(systemName: "pin.fill") .font(.caption) .foregroundStyle(.orange) } } } } // Deprecation/Disable warnings if formula.isDeprecated || formula.isDisabled { DeprecationDisableWarning( isDeprecated: formula.isDeprecated, deprecationReason: formula.deprecationReason, deprecationDate: formula.deprecationDate, isDisabled: formula.isDisabled, disableReason: formula.disableReason, disableDate: formula.disableDate, colorScheme: colorScheme ) } // Replacement suggestions ReplacementSuggestionsSection( deprecationReplacementFormula: formula.deprecationReplacementFormula, deprecationReplacementCask: formula.deprecationReplacementCask, disableReplacementFormula: formula.disableReplacementFormula, disableReplacementCask: formula.disableReplacementCask, brewManager: brewManager, colorScheme: colorScheme ) // Analytics if let analytics = analytics { AnalyticsSection(analytics: analytics, isCask: false, colorScheme: colorScheme) } Divider() // Service info if let service = formula.service { ServiceInfoSection(service: service, colorScheme: colorScheme) } // Basic info FormulaDetailsSectionView( formula: formula, fileCount: fileCount, totalSize: totalSize, colorScheme: colorScheme ) // Dependencies if (formula.dependencies != nil && !formula.dependencies!.isEmpty) || (formula.buildDependencies != nil && !formula.buildDependencies!.isEmpty) { DependenciesSection( runtimeDeps: formula.dependencies ?? [], buildDeps: formula.buildDependencies ?? [], installedFormulae: brewManager.installedFormulae.map { $0.name }, colorScheme: colorScheme ) } // Caveats if let caveats = formula.caveats { CaveatsSection(caveats: caveats, colorScheme: colorScheme) } } .padding() } .scrollIndicators(scrollIndicators ? .visible : .hidden) .onAppear { loadInstallationDetails() } // Install button pinned to bottom InstallButtonSection( packageName: formula.name, isCask: false, isInstalling: $isInstalling, isAlreadyInstalled: isAlreadyInstalled, showInstallAlert: $showInstallAlert, brewManager: brewManager, colorScheme: colorScheme ) .padding() } private func loadInstallationDetails() { // Only load if formula is installed guard isAlreadyInstalled else { return } Task { let brewPrefix = HomebrewController.shared.brewPrefix // Find the installed version from brewManager if let installedFormula = brewManager.installedFormulae.first(where: { $0.name == formula.name || $0.name == formula.name.components(separatedBy: "/").last }), let version = installedFormula.version { let cellarPath = "\(brewPrefix)/Cellar/\(formula.name)/\(version)" // Calculate file count and total size if let enumerator = FileManager.default.enumerator(atPath: cellarPath) { var count = 0 var size: Int64 = 0 while let _ = enumerator.nextObject() { count += 1 // Get file attributes for size if let fileAttributes = enumerator.fileAttributes, let fileSize = fileAttributes[.size] as? Int64 { size += fileSize } } await MainActor.run { fileCount = count totalSize = size } } } } } } // MARK: - Cask Details View struct CaskDetailsView: View { let cask: CaskDetails let analytics: HomebrewAnalytics? @Binding var isInstalling: Bool let isAlreadyInstalled: Bool @Binding var showInstallAlert: Bool let brewManager: HomebrewManager let colorScheme: ColorScheme @AppStorage("settings.interface.scrollIndicators") private var scrollIndicators: Bool = false var body: some View { ScrollView(.vertical, showsIndicators: scrollIndicators) { VStack(alignment: .leading, spacing: 16) { // Package name and version VStack(alignment: .leading, spacing: 4) { // Cask display name if let caskNames = cask.caskName, !caskNames.isEmpty { Text(caskNames.joined(separator: ", ")) .font(.title2) .fontWeight(.bold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } // Cask token Text(cask.name) .font(cask.caskName != nil ? .callout : .title2) .fontWeight(cask.caskName != nil ? .medium : .bold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) // Version with auto-updates badge if let version = cask.version { HStack(spacing: 4) { Text(verbatim: "v\(version)") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) if let autoUpdates = cask.autoUpdates, autoUpdates { Text(verbatim: "(auto_updates)") .font(.caption2) .foregroundStyle(.green) } } } } // Deprecation/Disable warnings if cask.isDeprecated || cask.isDisabled { DeprecationDisableWarning( isDeprecated: cask.isDeprecated, deprecationReason: cask.deprecationReason, deprecationDate: cask.deprecationDate, isDisabled: cask.isDisabled, disableReason: cask.disableReason, disableDate: cask.disableDate, colorScheme: colorScheme ) } // Replacement suggestions ReplacementSuggestionsSection( deprecationReplacementFormula: cask.deprecationReplacementFormula, deprecationReplacementCask: cask.deprecationReplacementCask, disableReplacementFormula: cask.disableReplacementFormula, disableReplacementCask: cask.disableReplacementCask, brewManager: brewManager, colorScheme: colorScheme ) // System requirements SystemRequirementsSection( minimumMacOSVersion: cask.minimumMacOSVersion, architectureRequirement: cask.architectureRequirement, colorScheme: colorScheme ) // Analytics if let analytics = analytics { AnalyticsSection(analytics: analytics, isCask: true, colorScheme: colorScheme) } Divider() // Basic info CaskDetailsSectionView(cask: cask, colorScheme: colorScheme) // Dependencies (formula dependencies for casks) if let dependencies = cask.dependencies, !dependencies.isEmpty { DependenciesSection( runtimeDeps: dependencies, buildDeps: [], installedFormulae: brewManager.installedFormulae.map { $0.name }, colorScheme: colorScheme ) } // Caveats if let caveats = cask.caveats { CaveatsSection(caveats: caveats, colorScheme: colorScheme) } } .padding() } .scrollIndicators(scrollIndicators ? .visible : .hidden) // Install button pinned to bottom InstallButtonSection( packageName: cask.name, isCask: true, isInstalling: $isInstalling, isAlreadyInstalled: isAlreadyInstalled, showInstallAlert: $showInstallAlert, brewManager: brewManager, colorScheme: colorScheme ) .padding() } } // MARK: - Package Header Section struct PackageHeaderSection: View { let package: HomebrewSearchResult let isCask: Bool let colorScheme: ColorScheme var body: some View { VStack(alignment: .leading, spacing: 8) { // Package name VStack(alignment: .leading, spacing: 4) { // Cask name (if available and different from token) if let caskNames = package.caskName, !caskNames.isEmpty { Text(caskNames.joined(separator: ", ")) .font(.title2) .fontWeight(.bold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } // Package token/name (use displayName if available and no caskName, otherwise show brew ID) Text(package.caskName == nil ? (package.displayName ?? package.name) : package.name) .font(package.caskName != nil ? .callout : .title2) .fontWeight(package.caskName != nil ? .medium : .bold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } // Version with auto-updates badge if let version = package.version { HStack(spacing: 4) { Text(verbatim: "v\(version)") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) if let autoUpdates = package.autoUpdates, autoUpdates { Text(verbatim: "(auto_updates)") .font(.caption2) .foregroundStyle(.green) } } } } } } // MARK: - Deprecation Warning Banner struct DeprecationWarningBanner: View { let deprecated: Bool let reason: String? let disableDate: String? let colorScheme: ColorScheme var body: some View { VStack(alignment: .leading, spacing: 4) { HStack { Image(systemName: "exclamationmark.triangle.fill") Text("DEPRECATED") .fontWeight(.bold) } .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) if let reason = reason { Text("Reason: \(formatReason(reason))") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } if let date = disableDate { Text("Will be disabled on \(date)") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } } .padding() .frame(maxWidth: .infinity, alignment: .leading) .background(Color.red.opacity(0.8)) .cornerRadius(8) } func formatReason(_ reason: String) -> String { switch reason { case "fails_gatekeeper_check": return "Does not pass macOS Gatekeeper check" default: return reason.replacingOccurrences(of: "_", with: " ").capitalized } } } // MARK: - Analytics Section struct AnalyticsSection: View { let analytics: HomebrewAnalytics let isCask: Bool let colorScheme: ColorScheme var body: some View { VStack(alignment: .leading, spacing: 12) { Text("📊 Popularity") .font(.caption) .fontWeight(.semibold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) HStack(spacing: 0) { // 365 days if let installs365d = analytics.install365d { VStack(spacing: 4) { Text(verbatim: "\(installs365d.formatted())") .font(.title3) .fontWeight(.bold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Text("365 days") .font(.caption2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .frame(maxWidth: .infinity) } if analytics.install365d != nil && analytics.install90d != nil { Divider() .frame(height: 40) } // 90 days if let installs90d = analytics.install90d { VStack(spacing: 4) { Text(verbatim: "\(installs90d.formatted())") .font(.title3) .fontWeight(.bold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Text("90 days") .font(.caption2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .frame(maxWidth: .infinity) } if analytics.install90d != nil && analytics.install30d != nil { Divider() .frame(height: 40) } // 30 days if let installs30d = analytics.install30d { VStack(spacing: 4) { Text(verbatim: "\(installs30d.formatted())") .font(.title3) .fontWeight(.bold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Text("30 days") .font(.caption2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .frame(maxWidth: .infinity) } } } .padding() .background(ThemeColors.shared(for: colorScheme).accent.opacity(0.1)) .cornerRadius(8) } } // MARK: - Details Section struct DetailsSectionView: View { let package: HomebrewSearchResult let isCask: Bool let colorScheme: ColorScheme var body: some View { VStack(alignment: .leading, spacing: 12) { // Description (only if present) if let description = package.description { VStack(alignment: .leading, spacing: 4) { Text("Description") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text(description) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } } // Homepage (only if present) if let homepage = package.homepage, let url = URL(string: homepage) { VStack(alignment: .leading, spacing: 4) { Text("Homepage") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Link(homepage, destination: url) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) } } // License (only if present) if let license = package.license { DetailRow(label: "License", value: license, colorScheme: colorScheme, isNA: false) } // Tap (only if present) if let tap = package.tap { DetailRow(label: "Tap", value: tap, colorScheme: colorScheme, isNA: false) } // Deprecated warning if package.isDeprecated { VStack(alignment: .leading, spacing: 4) { Text("⚠️ Deprecated") .font(.caption) .fontWeight(.semibold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) if let reason = package.deprecationReason { Text(reason) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } if let date = package.deprecationDate { Text("Since: \(date)") .font(.caption2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } .padding() .background(Color.orange.opacity(0.1)) .cornerRadius(8) } // Disabled warning if package.isDisabled { VStack(alignment: .leading, spacing: 4) { Text("🚫 Disabled") .font(.caption) .fontWeight(.semibold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) if let reason = package.disableReason { Text(reason) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } if let date = package.disableDate { Text("Since: \(date)") .font(.caption2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } .padding() .background(Color.red.opacity(0.1)) .cornerRadius(8) } // Installation type (formulae only, only if present) if !isCask, let isBottled = package.isBottled { DetailRow( label: "Installation", value: isBottled ? "Bottled (pre-built binary)" : "From source", colorScheme: colorScheme, isNA: false ) } // Keg-only (formulae only) if !isCask { if let isKegOnly = package.isKegOnly, isKegOnly { VStack(alignment: .leading, spacing: 4) { Text("🔒 Keg-Only") .font(.caption) .fontWeight(.semibold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Text(package.kegOnlyReason ?? "Not symlinked to Homebrew prefix") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .padding() .background(Color.blue.opacity(0.1)) .cornerRadius(8) } } // Requirements (only if present) if !isCask, let requirements = package.requirements { DetailRow(label: "Requirements", value: requirements, colorScheme: colorScheme, isNA: false) } // Conflicts (only if present) if let conflicts = package.conflictsWith, !conflicts.isEmpty { VStack(alignment: .leading, spacing: 4) { Text("⚠️ Conflicts With") .font(.caption) .fontWeight(.semibold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Text(conflicts.joined(separator: ", ")) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } .padding() .background(Color.yellow.opacity(0.1)) .cornerRadius(8) } // Artifacts (only if present) if isCask, let artifacts = package.artifacts, !artifacts.isEmpty { VStack(alignment: .leading, spacing: 4) { Text("Artifacts") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) ForEach(artifacts, id: \.self) { artifact in Text(verbatim: "• \(artifact)") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } } } // Aliases (only if present) if !isCask, let aliases = package.aliases, !aliases.isEmpty { DetailRow( label: "Aliases", value: aliases.joined(separator: ", "), colorScheme: colorScheme, isNA: false ) } // Versioned formulae (only if present) if !isCask, let versionedFormulae = package.versionedFormulae, !versionedFormulae.isEmpty { DetailRow( label: "Other Versions", value: versionedFormulae.joined(separator: ", "), colorScheme: colorScheme, isNA: false ) } // Optional dependencies (formulae only) if !isCask, let optionalDeps = package.optionalDependencies, !optionalDeps.isEmpty { VStack(alignment: .leading, spacing: 4) { Text("Optional Dependencies") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text(optionalDeps.joined(separator: ", ")) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } } // Recommended dependencies (formulae only) if !isCask, let recommendedDeps = package.recommendedDependencies, !recommendedDeps.isEmpty { VStack(alignment: .leading, spacing: 4) { Text("Recommended Dependencies") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text(recommendedDeps.joined(separator: ", ")) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } } // Uses from macOS (formulae only) if !isCask, let usesFromMacos = package.usesFromMacos, !usesFromMacos.isEmpty { VStack(alignment: .leading, spacing: 4) { Text("Uses from macOS") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text(usesFromMacos.joined(separator: ", ")) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } } // Download URL (casks only) if isCask, let url = package.url { VStack(alignment: .leading, spacing: 4) { Text("Download URL") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) if let urlObj = URL(string: url) { Link(url, destination: urlObj) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) .lineLimit(1) .truncationMode(.middle) } else { Text(url) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .lineLimit(1) .truncationMode(.middle) } } } // Appcast URL (casks only) if isCask, let appcast = package.appcast { VStack(alignment: .leading, spacing: 4) { Text("Appcast URL") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) if let urlObj = URL(string: appcast) { Link(appcast, destination: urlObj) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) .lineLimit(1) .truncationMode(.middle) } else { Text(appcast) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .lineLimit(1) .truncationMode(.middle) } } } } } } // MARK: - Dependencies Section struct DependenciesSection: View { let runtimeDeps: [String] let buildDeps: [String] let installedFormulae: [String] let colorScheme: ColorScheme var body: some View { VStack(alignment: .leading, spacing: 4) { Text("Dependencies") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) if !runtimeDeps.isEmpty { if !buildDeps.isEmpty { Text("Runtime:") .font(.caption2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .padding(.leading, 8) } ForEach(runtimeDeps, id: \.self) { dep in HStack(spacing: 4) { Text(verbatim: "•") Text(dep) if installedFormulae.contains(dep) { Text(verbatim: "✓") .foregroundStyle(.green) } else { Text(verbatim: "✗") .foregroundStyle(.red) } } .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .padding(.leading, 8) } } if !buildDeps.isEmpty { Text("Build only:") .font(.caption2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .padding(.leading, 8) .padding(.top, 4) ForEach(buildDeps, id: \.self) { dep in HStack(spacing: 4) { Text(verbatim: "•") Text(dep) if installedFormulae.contains(dep) { Text(verbatim: "✓") .foregroundStyle(.green) } else { Text(verbatim: "✗") .foregroundStyle(.red) } } .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .padding(.leading, 8) } } } } } // MARK: - Caveats Section struct CaveatsSection: View { let caveats: String let colorScheme: ColorScheme var body: some View { VStack(alignment: .leading, spacing: 4) { Text("Notes") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text(caveats) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } } } // MARK: - Install Button Section // MARK: - Detail Row Helper struct DetailRow: View { let label: String let value: String let colorScheme: ColorScheme var isNA: Bool = false var body: some View { VStack(alignment: .leading, spacing: 4) { Text(label) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text(value) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText.opacity(isNA ? 0.5 : 1.0)) } } } // MARK: - Installed Info Section struct InstalledInfoSection: View { let packageInfo: HomebrewPackageInfo let colorScheme: ColorScheme private var sizeText: String { if let sizeInBytes = packageInfo.sizeInBytes { return packageInfo.formattedSize(size: sizeInBytes) } else if let path = packageInfo.installedPath, let dirSize = directorySize(at: path) { let formatter = ByteCountFormatter() formatter.allowsNonnumericFormatting = false formatter.countStyle = .file return formatter.string(fromByteCount: dirSize) } return "size unknown" } var body: some View { VStack(alignment: .leading, spacing: 8) { Text("✅ Installed") .font(.caption) .fontWeight(.semibold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) if let path = packageInfo.installedPath, let count = packageInfo.fileCount { Text(verbatim: "\(path) (\(count) file\(count == 1 ? "" : "s"), \(sizeText))") .font(.caption2) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .textSelection(.enabled) } } .padding() .frame(maxWidth: .infinity, alignment: .leading) .background(Color.green.opacity(0.1)) .cornerRadius(8) } private func directorySize(at path: String) -> Int64? { guard let enumerator = FileManager.default.enumerator(atPath: path) else { return nil } var totalSize: Int64 = 0 let basePath = path while let file = enumerator.nextObject() as? String { let filePath = (basePath as NSString).appendingPathComponent(file) if let attributes = try? FileManager.default.attributesOfItem(atPath: filePath), let fileSize = attributes[.size] as? Int64 { totalSize += fileSize } } return totalSize > 0 ? totalSize : nil } } // MARK: - New Sections for Type-Safe Models // Combined Deprecation/Disable Warning struct DeprecationDisableWarning: View { let isDeprecated: Bool let deprecationReason: String? let deprecationDate: String? let isDisabled: Bool let disableReason: String? let disableDate: String? let colorScheme: ColorScheme var body: some View { VStack(spacing: 8) { if isDeprecated { VStack(alignment: .leading, spacing: 4) { HStack { Image(systemName: "exclamationmark.triangle.fill") Text("DEPRECATED") .fontWeight(.bold) } .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) if let reason = deprecationReason { Text("Reason: \(reason)") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } if let date = deprecationDate { Text("Since: \(date)") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } } .padding() .frame(maxWidth: .infinity, alignment: .leading) .background(Color.orange.opacity(0.8)) .cornerRadius(8) } if isDisabled { VStack(alignment: .leading, spacing: 4) { HStack { Image(systemName: "xmark.circle.fill") Text("DISABLED") .fontWeight(.bold) } .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) if let reason = disableReason { Text("Reason: \(reason)") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } if let date = disableDate { Text("Since: \(date)") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } } .padding() .frame(maxWidth: .infinity, alignment: .leading) .background(Color.red.opacity(0.8)) .cornerRadius(8) } } } } // Replacement Suggestions Section struct ReplacementSuggestionsSection: View { let deprecationReplacementFormula: String? let deprecationReplacementCask: String? let disableReplacementFormula: String? let disableReplacementCask: String? let brewManager: HomebrewManager let colorScheme: ColorScheme private var hasReplacements: Bool { deprecationReplacementFormula != nil || deprecationReplacementCask != nil || disableReplacementFormula != nil || disableReplacementCask != nil } var body: some View { if hasReplacements { VStack(alignment: .leading, spacing: 8) { Text("📦 Recommended Replacements") .font(.caption) .fontWeight(.semibold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) VStack(alignment: .leading, spacing: 4) { if let formula = deprecationReplacementFormula ?? disableReplacementFormula { ReplacementButton(name: formula, isCask: false, brewManager: brewManager, colorScheme: colorScheme) } if let cask = deprecationReplacementCask ?? disableReplacementCask { ReplacementButton(name: cask, isCask: true, brewManager: brewManager, colorScheme: colorScheme) } } } .padding() .background(ThemeColors.shared(for: colorScheme).accent.opacity(0.1)) .cornerRadius(8) } } } struct ReplacementButton: View { let name: String let isCask: Bool let brewManager: HomebrewManager let colorScheme: ColorScheme @State private var isInstalling = false var body: some View { Button { Task { isInstalling = true defer { isInstalling = false } do { try await HomebrewController.shared.installPackage(name: name, cask: isCask) // Targeted refresh - only update this newly installed package (much faster than full scan) await brewManager.refreshSpecificPackages([name]) } catch { printOS("Failed to install replacement \(name): \(error)") } } } label: { HStack(spacing: 6) { if isInstalling { ProgressView() .scaleEffect(0.7) } else { Image(systemName: "arrow.down.circle.fill") } Text("Install \(name)") Text(verbatim: "(\(isCask ? "cask" : "formula"))") .font(.caption2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) } .buttonStyle(.plain) .disabled(isInstalling) } } // System Requirements Section (Casks) struct SystemRequirementsSection: View { let minimumMacOSVersion: String? let architectureRequirement: ArchRequirement? let colorScheme: ColorScheme private var hasRequirements: Bool { minimumMacOSVersion != nil || architectureRequirement != nil } var body: some View { if hasRequirements { VStack(alignment: .leading, spacing: 8) { Text("💻 System Requirements") .font(.caption) .fontWeight(.semibold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) VStack(alignment: .leading, spacing: 4) { if let macOSVersion = minimumMacOSVersion { HStack(spacing: 4) { Text(verbatim: "•") Text(verbatim: "macOS: \(macOSVersion)") } .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } if let arch = architectureRequirement { HStack(spacing: 4) { Text(verbatim: "•") Text("Architecture: \(arch.displayName)") } .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } } } .padding() .background(Color.blue.opacity(0.1)) .cornerRadius(8) } } } // Service Info Section (Formulae) struct ServiceInfoSection: View { let service: ServiceInfo let colorScheme: ColorScheme var body: some View { VStack(alignment: .leading, spacing: 8) { Text("⚙️ Service/Daemon") .font(.caption) .fontWeight(.semibold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) VStack(alignment: .leading, spacing: 4) { if let run = service.run, !run.isEmpty { VStack(alignment: .leading, spacing: 2) { Text("Run Command:") .font(.caption2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text(run.joined(separator: " ")) .font(.caption) .fontWeight(.medium) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } } if let runType = service.runType { HStack(spacing: 4) { Text(verbatim: "•") Text("Run Type: \(runType)") } .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } if let workingDir = service.workingDir { HStack(spacing: 4) { Text(verbatim: "•") Text("Working Directory: \(workingDir)") } .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } if let keepAlive = service.keepAlive, keepAlive { HStack(spacing: 4) { Text(verbatim: "•") Text("Keep Alive: Always") } .font(.caption) .foregroundStyle(.green) } } } .padding() .background(Color.purple.opacity(0.1)) .cornerRadius(8) } } // Formula-specific details section struct FormulaDetailsSectionView: View { let formula: FormulaDetails let fileCount: Int? let totalSize: Int64? let colorScheme: ColorScheme var body: some View { VStack(alignment: .leading, spacing: 12) { // Description if let description = formula.description { VStack(alignment: .leading, spacing: 4) { Text("Description") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text(description) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } } // Homepage if let homepage = formula.homepage, let url = URL(string: homepage) { VStack(alignment: .leading, spacing: 4) { Text("Homepage") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Link(homepage, destination: url) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) } } // License if let license = formula.license { DetailRow(label: "License", value: license, colorScheme: colorScheme, isNA: false) } // Tap if let tap = formula.tap { DetailRow(label: "Tap", value: tap, colorScheme: colorScheme, isNA: false) } // Installation type if let isBottled = formula.isBottled { VStack(alignment: .leading, spacing: 4) { Text("Installation") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) VStack(alignment: .leading, spacing: 2) { Text(isBottled ? "Bottled (pre-built binary)" : "From source") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) // Show file count and size if available if let count = fileCount, let size = totalSize { Text(installationSizeText(count: count, size: size)) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } } } // Keg-only if let isKegOnly = formula.isKegOnly, isKegOnly { VStack(alignment: .leading, spacing: 4) { Text("🔒 Keg-Only") .font(.caption) .fontWeight(.semibold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Text(formula.kegOnlyReason ?? "Not symlinked to Homebrew prefix") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .padding() .background(Color.blue.opacity(0.1)) .cornerRadius(8) } // Requirements if let requirements = formula.requirements { DetailRow(label: "Requirements", value: requirements, colorScheme: colorScheme, isNA: false) } // Conflicts if let conflicts = formula.conflictsWith, !conflicts.isEmpty { VStack(alignment: .leading, spacing: 4) { Text("⚠️ Conflicts With") .font(.caption) .fontWeight(.semibold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) if let reasons = formula.conflictsWithReasons, reasons.count == conflicts.count { // Show conflicts with their reasons VStack(alignment: .leading, spacing: 2) { ForEach(Array(zip(conflicts, reasons)), id: \.0) { conflict, reason in Text(verbatim: "\(conflict): \(reason)") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } } } else { // No reasons available, just show conflicts Text(conflicts.joined(separator: ", ")) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } } .padding() .background(Color.yellow.opacity(0.1)) .cornerRadius(8) } // Aliases if let aliases = formula.aliases, !aliases.isEmpty { DetailRow( label: "Aliases", value: aliases.joined(separator: ", "), colorScheme: colorScheme, isNA: false ) } // Versioned formulae if let versionedFormulae = formula.versionedFormulae, !versionedFormulae.isEmpty { DetailRow( label: "Other Versions", value: versionedFormulae.joined(separator: ", "), colorScheme: colorScheme, isNA: false ) } // Optional dependencies if let optionalDeps = formula.optionalDependencies, !optionalDeps.isEmpty { VStack(alignment: .leading, spacing: 4) { Text("Optional Dependencies") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text(optionalDeps.joined(separator: ", ")) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } } // Recommended dependencies if let recommendedDeps = formula.recommendedDependencies, !recommendedDeps.isEmpty { VStack(alignment: .leading, spacing: 4) { Text("Recommended Dependencies") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text(recommendedDeps.joined(separator: ", ")) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } } // Uses from macOS if let usesFromMacos = formula.usesFromMacos, !usesFromMacos.isEmpty { VStack(alignment: .leading, spacing: 4) { Text("Uses from macOS") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text(usesFromMacos.joined(separator: ", ")) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } } } } private func installationSizeText(count: Int, size: Int64) -> String { let formatter = ByteCountFormatter() formatter.allowsNonnumericFormatting = false formatter.countStyle = .file let sizeString = formatter.string(fromByteCount: size) return "(\(count) files, \(sizeString))" } } // Cask-specific details section struct CaskDetailsSectionView: View { let cask: CaskDetails let colorScheme: ColorScheme var body: some View { VStack(alignment: .leading, spacing: 12) { // Description if let description = cask.description { VStack(alignment: .leading, spacing: 4) { Text("Description") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text(description) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } } // Homepage if let homepage = cask.homepage, let url = URL(string: homepage) { VStack(alignment: .leading, spacing: 4) { Text("Homepage") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Link(homepage, destination: url) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) } } // License if let license = cask.license { DetailRow(label: "License", value: license, colorScheme: colorScheme, isNA: false) } // Tap if let tap = cask.tap { DetailRow(label: "Tap", value: tap, colorScheme: colorScheme, isNA: false) } // Conflicts if let conflicts = cask.conflictsWith, !conflicts.isEmpty { VStack(alignment: .leading, spacing: 4) { Text("⚠️ Conflicts With") .font(.caption) .fontWeight(.semibold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) if let reasons = cask.conflictsWithReasons, reasons.count == conflicts.count { // Show conflicts with their reasons VStack(alignment: .leading, spacing: 2) { ForEach(Array(zip(conflicts, reasons)), id: \.0) { conflict, reason in Text(verbatim: "\(conflict): \(reason)") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } } } else { // No reasons available, just show conflicts Text(conflicts.joined(separator: ", ")) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } } .padding() .background(Color.yellow.opacity(0.1)) .cornerRadius(8) } // Artifacts if let artifacts = cask.artifacts, !artifacts.isEmpty { VStack(alignment: .leading, spacing: 4) { Text("Artifacts") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) ForEach(artifacts, id: \.self) { artifact in Text(verbatim: "• \(artifact)") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } } } // Download URL if let url = cask.url { VStack(alignment: .leading, spacing: 4) { Text("Download URL") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) if let urlObj = URL(string: url) { Link(url, destination: urlObj) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) .lineLimit(1) .truncationMode(.middle) } else { Text(url) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .lineLimit(1) .truncationMode(.middle) } } } // Appcast URL if let appcast = cask.appcast { VStack(alignment: .leading, spacing: 4) { Text("Appcast URL") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) if let urlObj = URL(string: appcast) { Link(appcast, destination: urlObj) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) .lineLimit(1) .truncationMode(.middle) } else { Text(appcast) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .lineLimit(1) .truncationMode(.middle) } } } } } } // Updated InstallButtonSection to accept packageName instead of package object struct InstallButtonSection: View { let packageName: String let isCask: Bool @Binding var isInstalling: Bool let isAlreadyInstalled: Bool @Binding var showInstallAlert: Bool let brewManager: HomebrewManager let colorScheme: ColorScheme var body: some View { HStack { Spacer() if isInstalling { HStack(spacing: 8) { ProgressView() .scaleEffect(0.9) Text("Installing...") .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } else if isAlreadyInstalled { Text("Installed") .font(.callout) .fontWeight(.medium) .foregroundStyle(.green) } else { Button("Install") { showInstallAlert = true } .buttonStyle(.borderedProminent) .tint(ThemeColors.shared(for: colorScheme).accent) } Spacer() } .frame(height: 44) .alert("Install \(packageName)?", isPresented: $showInstallAlert) { Button("Cancel", role: .cancel) { } Button("Install") { Task { await MainActor.run { isInstalling = true } defer { Task { @MainActor in isInstalling = false } } do { try await HomebrewController.shared.installPackage(name: packageName, cask: isCask) // Targeted refresh - only update this newly installed package (much faster than full scan) await brewManager.refreshSpecificPackages([packageName]) // Refresh AppState.sortedApps to include newly installed GUI app (casks only) if isCask { await loadAndAddCaskApp(caskName: packageName) } } catch { printOS("Error installing package \(packageName): \(error)") } } } } message: { Text("This will install \(packageName) using Homebrew. This may take several minutes.") } } } // Installed category view component (matches Updater view's CategorySection pattern) struct InstalledCategoryView: View { let category: InstalledCategory let packages: [HomebrewSearchResult] let isLoading: Bool let collapsed: Bool let onToggle: () -> Void let isFirst: Bool let onPackageSelected: (HomebrewSearchResult, Bool) -> Void let updatingPackages: Set let brewManager: HomebrewManager let onUpdateAll: (() -> Void)? let colorScheme: ColorScheme @Binding var showOnlyInstalledOnRequest: Bool @Binding var sortOption: BrewPackageSortOption private var sortedPackages: [HomebrewSearchResult] { switch sortOption { case .name: return packages.sorted { ($0.displayName ?? $0.name).sortKey < ($1.displayName ?? $1.name).sortKey } case .size: // Sort by size bytes (largest first) // Get size from InstalledPackage return packages.sorted { pkg1, pkg2 in let size1 = brewManager.installedFormulae.first(where: { $0.name == pkg1.name })?.sizeBytes ?? brewManager.installedCasks.first(where: { $0.name == pkg1.name })?.sizeBytes ?? 0 let size2 = brewManager.installedFormulae.first(where: { $0.name == pkg2.name })?.sizeBytes ?? brewManager.installedCasks.first(where: { $0.name == pkg2.name })?.sizeBytes ?? 0 return size1 > size2 } } } var body: some View { VStack(alignment: .leading, spacing: 8) { // Category header (collapsible) Button(action: onToggle) { HStack { Image(systemName: collapsed ? "chevron.right" : "chevron.down") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .frame(width: 10) .opacity(packages.isEmpty ? 0 : 1) Image(systemName: category.icon) .font(.headline) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) .frame(width: 20) Text(category.rawValue) .font(.headline) .fontWeight(.semibold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) // Show progress spinner while loading, otherwise show count if isLoading { ProgressView() .controlSize(.small) } else { Text(verbatim: "(\(packages.count))") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } // Show "installed on request" filter toggle only for Formulae category if category == .formulae { Button { showOnlyInstalledOnRequest.toggle() } label: { Image(systemName: showOnlyInstalledOnRequest ? "leaf.fill" : "leaf") // .font(.caption) .foregroundStyle(showOnlyInstalledOnRequest ? .green : ThemeColors.shared(for: colorScheme).secondaryText) } .buttonStyle(.plain) .contentShape(Rectangle()) .help("Show only formulae installed on request (not as dependencies)") } Spacer() // Sort menu Menu { ForEach(BrewPackageSortOption.allCases, id: \.self) { option in Button { sortOption = option } label: { Label(option.rawValue, systemImage: option.systemImage) } } } label: { Label(sortOption.rawValue, systemImage: sortOption.systemImage) .font(.caption) } .labelStyle(.titleAndIcon) .buttonStyle(.plain) .help("Sort by \(sortOption.rawValue)") // Show "Update All" button if provided if let onUpdateAll = onUpdateAll { Button { onUpdateAll() } label: { Text("Update All") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) } .buttonStyle(.plain) } } } .buttonStyle(.plain) .contentShape(Rectangle()) .padding(.top, isFirst ? 0 : 20) // Packages in category (only if not collapsed) if !collapsed { LazyVStack(spacing: 8) { ForEach(sortedPackages) { result in SearchResultRowView( result: result, isCask: brewManager.installedCasks.contains(where: { $0.name == result.name }), onInfoTapped: { let isCask = brewManager.installedCasks.contains(where: { $0.name == result.name }) onPackageSelected(result, isCask) }, updatingPackages: updatingPackages ) } } } } } } // Available category view component (matches Installed/Updater view pattern) struct AvailableCategoryView: View { let category: AvailableCategory let packages: [HomebrewSearchResult] let collapsed: Bool let onToggle: () -> Void let isFirst: Bool let onPackageSelected: (HomebrewSearchResult, Bool) -> Void let updatingPackages: Set let brewManager: HomebrewManager let colorScheme: ColorScheme var body: some View { VStack(alignment: .leading, spacing: 8) { // Category header (collapsible) Button(action: onToggle) { HStack { Image(systemName: collapsed ? "chevron.right" : "chevron.down") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .frame(width: 10) Image(systemName: category.icon) .font(.headline) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) .frame(width: 20) Text(category.rawValue) .font(.headline) .fontWeight(.semibold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Text(verbatim: "(\(packages.count))") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() } } .buttonStyle(.plain) .contentShape(Rectangle()) .padding(.top, isFirst ? 0 : 20) // Packages in category (only if not collapsed) if !collapsed { LazyVStack(spacing: 8) { ForEach(packages) { result in SearchResultRowView( result: result, isCask: category == .casks, onInfoTapped: { onPackageSelected(result, category == .casks) }, updatingPackages: updatingPackages ) } } } } } } // MARK: - Helper Functions /// Find AppInfo matching the given cask identifier /// Used for optimized bundle flushing after Homebrew operations fileprivate func findAppByCask(_ caskName: String) -> AppInfo? { let shortName = caskName.components(separatedBy: "/").last ?? caskName return AppState.shared.sortedApps.first(where: { appInfo in appInfo.cask == caskName || appInfo.cask == shortName }) } /// Load a newly installed cask app from Caskroom and add it to sortedApps /// Used after installing new casks to sync AppState without full rescan fileprivate func loadAndAddCaskApp(caskName: String) async { let brewPrefix = HomebrewController.shared.brewPrefix let caskroomPath = "\(brewPrefix)/Caskroom/\(caskName)" let globPattern = "\(caskroomPath)/*/*.app" // Find the app symlink in Caskroom using glob var globResult = glob_t() defer { globfree(&globResult) } guard glob(globPattern, 0, nil, &globResult) == 0 else { printOS("No app found in Caskroom for cask: \(caskName)") return } guard globResult.gl_pathc > 0, let cPath = globResult.gl_pathv[0], let symlinkPath = String(validatingUTF8: cPath) else { printOS("No valid app path found for cask: \(caskName)") return } // Resolve symlink to get real path in /Applications let realPath = URL(fileURLWithPath: symlinkPath).resolvingSymlinksInPath() // Flush bundle cache to avoid stale cached data (important for reinstalls) flushBundleCache(for: realPath) // Load app info from real path guard let appInfo = AppInfoFetcher.getAppInfo(atPath: realPath) else { printOS("Failed to load app info for: \(realPath.path)") return } // Add to sortedApps array and re-sort await MainActor.run { AppState.shared.sortedApps.append(appInfo) AppState.shared.sortedApps.sort { $0.appName < $1.appName } invalidateCaskLookupCache() } } /// Load an upgraded cask app from Caskroom and update it in sortedApps /// Used after upgrading existing casks to sync AppState with new version fileprivate func loadAndUpdateCaskApp(caskName: String) async { let brewPrefix = HomebrewController.shared.brewPrefix let caskroomPath = "\(brewPrefix)/Caskroom/\(caskName)" let globPattern = "\(caskroomPath)/*/*.app" // Find the app symlink in Caskroom using glob var globResult = glob_t() defer { globfree(&globResult) } guard glob(globPattern, 0, nil, &globResult) == 0 else { printOS("No app found in Caskroom for cask: \(caskName)") return } guard globResult.gl_pathc > 0, let cPath = globResult.gl_pathv[0], let symlinkPath = String(validatingUTF8: cPath) else { printOS("No valid app path found for cask: \(caskName)") return } // Resolve symlink to get real path in /Applications let realPath = URL(fileURLWithPath: symlinkPath).resolvingSymlinksInPath() // Flush bundle cache to ensure we get updated version info flushBundleCache(for: realPath) // Load app info from real path with updated version guard let newAppInfo = AppInfoFetcher.getAppInfo(atPath: realPath) else { printOS("Failed to load app info for: \(realPath.path)") return } // Replace existing entry in sortedApps array and re-sort await MainActor.run { if let index = AppState.shared.sortedApps.firstIndex(where: { $0.bundleIdentifier == newAppInfo.bundleIdentifier }) { AppState.shared.sortedApps[index] = newAppInfo } else { // Fallback: if not found, just append (shouldn't happen for upgrades) AppState.shared.sortedApps.append(newAppInfo) } AppState.shared.sortedApps.sort { $0.appName < $1.appName } invalidateCaskLookupCache() } } // MARK: - Error Handlers (Free Functions) private func handleBrewInstallError(_ error: HomebrewError, packageName: String, isCask: Bool, brewManager: HomebrewManager) { switch error { case .appAlreadyExists(_, let path): showCustomAlert( title: "App Already Exists", message: "An app already exists at \(path). Would you like to force install and overwrite it?", style: .warning, onOk: { Task { do { try await HomebrewController.shared.installPackage(name: packageName, cask: isCask, force: true) await brewManager.refreshSpecificPackages([packageName]) } catch { printOS("Error force installing \(packageName): \(error)") } } } ) case .formulaConflict(_, let conflicts): showCustomAlert( title: "Formula Conflict", message: "Cannot install \(packageName) because it conflicts with: \(conflicts). Would you like to force install anyway?", style: .warning, onOk: { Task { do { try await HomebrewController.shared.installPackage(name: packageName, cask: isCask, force: true) await brewManager.refreshSpecificPackages([packageName]) } catch { printOS("Error force installing \(packageName): \(error)") } } } ) default: showCustomAlert( title: "Installation Error", message: error.localizedDescription, style: .critical ) } } private func handleBrewUninstallError(_ error: HomebrewError, packageName: String, brewManager: HomebrewManager) { switch error { case .dependencyConflict(_, let dependents): let depList = dependents.joined(separator: ", ") showCustomAlert( title: "Dependency Conflict", message: "Cannot uninstall \(packageName) because it is required by: \(depList). Would you like to uninstall it anyway (ignoring dependencies)?", style: .warning, onOk: { Task { do { try await HomebrewController.shared.uninstallPackage(name: packageName, ignoreDependencies: true) let shortName = packageName.components(separatedBy: "/").last ?? packageName await MainActor.run { brewManager.installedCasks.removeAll { $0.name == packageName || $0.name == shortName } brewManager.installedFormulae.removeAll { $0.name == packageName || $0.name == shortName } brewManager.outdatedPackagesMap.removeValue(forKey: packageName) brewManager.outdatedPackagesMap.removeValue(forKey: shortName) brewManager.updateInstalledCategories() } } catch { printOS("Error uninstalling \(packageName) with --ignore-dependencies: \(error)") } } } ) default: showCustomAlert( title: "Uninstall Error", message: error.localizedDescription, style: .critical ) } } ================================================ FILE: Pearcleaner/Views/Brew/TapManagementSection.swift ================================================ // // TapManagementSection.swift // Pearcleaner // // Created by Alin Lupascu on 10/01/25. // import SwiftUI import AlinFoundation struct TapManagementSection: View { @EnvironmentObject var brewManager: HomebrewManager @Environment(\.colorScheme) var colorScheme @State private var newTapName: String = "" @State private var isAddingTap: Bool = false @State private var showAddTapSheet: Bool = false @AppStorage("settings.interface.scrollIndicators") private var scrollIndicators: Bool = false var body: some View { VStack(alignment: .leading, spacing: 0) { // Header with add button HStack { Text(verbatim: "\(brewManager.availableTaps.count) tap\(brewManager.availableTaps.count == 1 ? "" : "s")") .font(.caption) .monospacedDigit() .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) if brewManager.isLoadingTaps { Text("Loading...") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } Spacer() Button { showAddTapSheet = true } label: { Label("Add Tap", systemImage: "plus") } .buttonStyle(ControlGroupButtonStyle( foregroundColor: ThemeColors.shared(for: colorScheme).accent, shape: Capsule(style: .continuous), level: .primary, skipControlGroup: true )) } .padding(.horizontal, 20) .padding(.vertical, 10) if brewManager.isLoadingTaps { VStack(alignment: .center, spacing: 10) { Spacer() ProgressView() .scaleEffect(1.5) Text("Loading taps...") .font(.title2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() } .frame(maxWidth: .infinity) } else if brewManager.availableTaps.isEmpty { VStack(alignment: .center, spacing: 15) { Spacer() Image(systemName: "point.3.filled.connected.trianglepath.dotted") .font(.system(size: 50)) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text("No taps found") .font(.title2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text("Add a tap to get started") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() } .frame(maxWidth: .infinity) } else { ScrollView { LazyVStack(spacing: 8) { ForEach(brewManager.availableTaps) { tap in TapRowView(tap: tap) } } .padding(.horizontal, 20) .padding(.bottom, 20) } .scrollIndicators(scrollIndicators ? .automatic : .never) } } .sheet(isPresented: $showAddTapSheet) { AddTapSheet(isPresented: $showAddTapSheet) } } } // MARK: - Tap Row View struct TapRowView: View { let tap: HomebrewTapInfo @EnvironmentObject var brewManager: HomebrewManager @Environment(\.colorScheme) var colorScheme @State private var isHovered: Bool = false @State private var isRemoving: Bool = false @State private var showRemoveAlert: Bool = false @State private var isExpanded: Bool = false @State private var isLoadingPackages: Bool = false @State private var tapFormulae: [String] = [] @State private var tapCasks: [String] = [] var body: some View { VStack(spacing: 0) { HStack(alignment: .center, spacing: 12) { // Tap icon ZStack { Circle() .fill((tap.isOfficial ? Color.blue : Color.orange).opacity(0.2)) .frame(width: 40, height: 40) Image(systemName: tap.isOfficial ? "mug" : "point.3.filled.connected.trianglepath.dotted") .font(.system(size: 18, weight: .medium)) .foregroundStyle(tap.isOfficial ? .blue : .orange) } // Tap name and details VStack(alignment: .leading, spacing: 4) { Text(tap.name) .font(.headline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) HStack(spacing: 6) { if tap.isOfficial { Label("Official", systemImage: "checkmark.seal.fill") .font(.caption) .foregroundStyle(.blue) } else { Text("Third-party") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } Text(verbatim: "•") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text(verbatim: "\(HomebrewController.shared.getBrewPrefix())/Library/Taps/\(tap.name.replacingOccurrences(of: "/", with: "/homebrew-"))") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .lineLimit(1) } } Spacer() // See Packages button Button { withAnimation(.easeInOut(duration: 0.2)) { isExpanded.toggle() } if isExpanded { // Load packages after expanding Task { await loadTapPackages() } } } label: { Label(isExpanded ? "Hide Packages" : "See Packages", systemImage: isExpanded ? "chevron.up" : "chevron.down") } .buttonStyle(.borderless) .help("View packages in this tap") // Remove button if isRemoving { HStack(spacing: 8) { ProgressView() .scaleEffect(0.8) Text("Removing...") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } else { //if !tap.isOfficial { Button { showRemoveAlert = true } label: { Label("Remove", systemImage: "trash") .foregroundStyle(.red) } .buttonStyle(.borderless) .help("Remove tap") } } .padding() // Expanded packages section if isExpanded { VStack(alignment: .leading, spacing: 12) { Divider() .padding(.horizontal) if isLoadingPackages { HStack { Spacer() ProgressView() .scaleEffect(0.8) Text("Loading packages...") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() } .padding(.vertical, 8) } else if tapFormulae.isEmpty && tapCasks.isEmpty { HStack { Spacer() Text("No packages found in this tap") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() } .padding(.vertical, 8) } else { VStack(alignment: .leading, spacing: 8) { // Show formulae if !tapFormulae.isEmpty { Text("Formulae (\(tapFormulae.count))") .font(.caption) .fontWeight(.semibold) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .padding(.horizontal) ForEach(tapFormulae, id: \.self) { formulaName in TapPackageRowView(tapName: tap.name, packageName: formulaName, isCask: false) } } // Show casks if !tapCasks.isEmpty { Text("Casks (\(tapCasks.count))") .font(.caption) .fontWeight(.semibold) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .padding(.horizontal) .padding(.top, tapFormulae.isEmpty ? 0 : 8) ForEach(tapCasks, id: \.self) { caskName in TapPackageRowView(tapName: tap.name, packageName: caskName, isCask: true) } } } .padding(.bottom, 8) } } .padding(.horizontal) } } .background( RoundedRectangle(cornerRadius: 8) .fill(isHovered ? ThemeColors.shared(for: colorScheme).secondaryBG.opacity(0.8) : ThemeColors.shared(for: colorScheme).secondaryBG ) ) .onHover { hovering in withAnimation(.easeInOut(duration: 0.2)) { isHovered = hovering } } .alert("Remove \(tap.name)?", isPresented: $showRemoveAlert) { Button("Cancel", role: .cancel) { } Button("Remove", role: .destructive) { Task { @MainActor in isRemoving = true defer { isRemoving = false } do { // Always use force flag - leaves packages installed try await HomebrewController.shared.removeTap(name: tap.name, force: true) // Just remove from list instead of reloading all taps brewManager.removeTapFromList(name: tap.name) // Don't reload installed packages - they're still there } catch { printOS("Error removing tap: \(error)") } } } } message: { Text("Are you sure you want to remove this tap?") } } private func loadTapPackages() async { isLoadingPackages = true defer { isLoadingPackages = false } do { let result = try await HomebrewController.shared.getPackagesFromTap(tap.name) tapFormulae = result.formulae tapCasks = result.casks } catch { printOS("Error loading packages from tap: \(error)") tapFormulae = [] tapCasks = [] } } } // MARK: - Add Tap Sheet struct AddTapSheet: View { @Binding var isPresented: Bool @EnvironmentObject var brewManager: HomebrewManager @Environment(\.colorScheme) var colorScheme @State private var tapName: String = "" @State private var isAdding: Bool = false @State private var errorMessage: String = "" var body: some View { StandardSheetView( title: "Add Tap", width: 450, height: 400, onClose: { isPresented = false } ) { // Content VStack(alignment: .leading, spacing: 8) { Text("Enter the tap name to add") .font(.subheadline) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text("Examples:") .font(.caption) .fontWeight(.semibold) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) VStack(alignment: .leading, spacing: 4) { Text(verbatim: "• homebrew/cask-versions") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .monospaced() Text(verbatim: "• user/tap") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .monospaced() Text(verbatim: "• organization/repository") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .monospaced() } } .frame(maxWidth: .infinity, alignment: .leading) .padding() .background( RoundedRectangle(cornerRadius: 8) .fill(ThemeColors.shared(for: colorScheme).secondaryBG) ) TextField("user/tap", text: $tapName) .textFieldStyle(.plain) .padding(.horizontal, 12) .padding(.vertical, 8) .background( RoundedRectangle(cornerRadius: 8) .fill(ThemeColors.shared(for: colorScheme).secondaryBG) ) if !errorMessage.isEmpty { Text(errorMessage) .font(.caption) .foregroundStyle(.red) .frame(maxWidth: .infinity, alignment: .leading) } } actionButtons: { HStack(spacing: 12) { Button("Cancel") { isPresented = false } .buttonStyle(ControlGroupButtonStyle( foregroundColor: ThemeColors.shared(for: colorScheme).secondaryText, shape: Capsule(style: .continuous), level: .secondary, skipControlGroup: true )) Button(isAdding ? "Adding..." : "Add Tap") { addTap() } .buttonStyle(ControlGroupButtonStyle( foregroundColor: ThemeColors.shared(for: colorScheme).accent, shape: Capsule(style: .continuous), level: .primary, skipControlGroup: true )) .disabled(tapName.isEmpty || isAdding) } } } private func addTap() { errorMessage = "" Task { isAdding = true do { try await HomebrewController.shared.addTap(name: tapName) await brewManager.loadTaps() // Note: No need to update Browse cache - we load names dynamically now isPresented = false } catch { errorMessage = "Failed to add tap: \(error.localizedDescription)" } isAdding = false } } } // MARK: - Tap Package Row View struct TapPackageRowView: View { let tapName: String let packageName: String let isCask: Bool @EnvironmentObject var brewManager: HomebrewManager @Environment(\.colorScheme) var colorScheme @AppStorage("settings.general.confirmAlert") private var confirmAlert: Bool = false @State private var isInstalling: Bool = false @State private var isUninstalling: Bool = false @State private var showInstallAlert: Bool = false @State private var showUninstallAlert: Bool = false @State private var isHovered: Bool = false // Fully-qualified name for installation (e.g., "powershell/tap/powershell") private var fullPackageName: String { "\(tapName)/\(packageName)" } private var isAlreadyInstalled: Bool { if isCask { return brewManager.installedCasks.contains { installedPackage in installedPackage.name == packageName } } else { return brewManager.installedFormulae.contains { installedPackage in installedPackage.name == packageName } } } var body: some View { HStack(alignment: .center, spacing: 8) { // Package icon (smaller) ZStack { Circle() .fill((isCask ? Color.purple : Color.green).opacity(0.15)) .frame(width: 28, height: 28) Image(systemName: isCask ? "shippingbox.fill" : "cube.fill") .font(.system(size: 12, weight: .medium)) .foregroundStyle(isCask ? .purple : .green) } // Package info VStack(alignment: .leading, spacing: 2) { Text(packageName) .font(.caption) .fontWeight(.medium) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } Spacer() // Install status/button if isInstalling { HStack(spacing: 4) { ProgressView() .scaleEffect(0.6) Text("Installing...") .font(.caption2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } else if isUninstalling { HStack(spacing: 4) { ProgressView() .scaleEffect(0.6) Text("Uninstalling...") .font(.caption2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } else if isAlreadyInstalled { HStack(spacing: 8) { HStack(spacing: 4) { Image(systemName: "checkmark.circle.fill") .font(.caption2) .foregroundStyle(.green) Text("Installed") .font(.caption2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } // Uninstall button Button { if confirmAlert { showUninstallAlert = true } else { performUninstall() } } label: { Image(systemName: "trash") .font(.caption2) .foregroundStyle(.red) } .buttonStyle(.borderless) .help("Uninstall package") } } else { Button("Install") { if confirmAlert { showInstallAlert = true } else { performInstall() } } .font(.caption2) .buttonStyle(ControlGroupButtonStyle( foregroundColor: ThemeColors.shared(for: colorScheme).accent, shape: Capsule(style: .continuous), level: .primary, skipControlGroup: true )) } } .padding(.horizontal, 12) .padding(.vertical, 6) .background( RoundedRectangle(cornerRadius: 6) .fill(isHovered ? ThemeColors.shared(for: colorScheme).secondaryBG.opacity(0.5) : Color.clear ) ) .onHover { hovering in withAnimation(.easeInOut(duration: 0.15)) { isHovered = hovering } } .alert("Install \(packageName)?", isPresented: $showInstallAlert) { Button("Cancel", role: .cancel) { } Button("Install") { Task { @MainActor in isInstalling = true defer { isInstalling = false } do { try await HomebrewController.shared.installPackage(name: fullPackageName, cask: isCask) if isCask { invalidateCaskLookupCache() } await brewManager.loadInstalledPackages() } catch { printOS("Error installing package \(fullPackageName): \(error)") } } } } message: { Text("This will install \(packageName) from the tapped repository.") } .alert("Uninstall \(packageName)?", isPresented: $showUninstallAlert) { Button("Cancel", role: .cancel) { } Button("Uninstall", role: .destructive) { Task { @MainActor in isUninstalling = true defer { isUninstalling = false } do { try await HomebrewUninstaller.shared.uninstallPackage(name: packageName, cask: isCask) if isCask { invalidateCaskLookupCache() } await brewManager.loadInstalledPackages() } catch { printOS("Error uninstalling package \(packageName): \(error)") } } } } message: { Text("This will remove \(packageName) from your system.") } } // MARK: - Helper Functions private func performInstall() { Task { @MainActor in isInstalling = true defer { isInstalling = false } do { try await HomebrewController.shared.installPackage(name: fullPackageName, cask: isCask) if isCask { invalidateCaskLookupCache() } await brewManager.loadInstalledPackages() } catch { printOS("Error installing package \(fullPackageName): \(error)") } } } private func performUninstall() { Task { @MainActor in isUninstalling = true defer { isUninstalling = false } do { try await HomebrewUninstaller.shared.uninstallPackage(name: packageName, cask: isCask) if isCask { invalidateCaskLookupCache() } await brewManager.loadInstalledPackages() } catch { printOS("Error uninstalling package \(packageName): \(error)") } } } } ================================================ FILE: Pearcleaner/Views/Components/BadgeOverlay.swift ================================================ // // BadgeOverlay.swift // Pearcleaner // // Unified overlay component for all toolbar badge notifications // Created by Alin Lupascu on 10/23/25. // import SwiftUI import AlinFoundation enum OverlayType { case helper case permissions case update case announcement } struct BadgeOverlay: View { @EnvironmentObject var updater: Updater @ObservedObject var helperManager = HelperToolManager.shared @ObservedObject var permissionManager = PermissionManagerLocal.shared @Environment(\.colorScheme) var colorScheme @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true @AppStorage("settings.interface.badgeOverlaysEnabled") private var badgeOverlaysEnabled: Bool = true @State private var isVisible: Bool = false @State private var isExpanded: Bool = false @State private var showPermissionList: Bool = false @State private var showUpdateView: Bool = false @State private var showFeatureView: Bool = false @State private var dismissedOverlays: Set = [] // Determine which overlay to show based on priority (helper → permissions → update → announcement) private var currentOverlay: OverlayType? { // Helper: highest priority if helperManager.shouldShowHelperBadge { if !dismissedOverlays.contains(.helper) { return .helper } } // Permissions: second priority if permissionManager.shouldShowPermissionWarning { if !dismissedOverlays.contains(.permissions) { return .permissions } } // Update: third priority if updater.updateAvailable && !dismissedOverlays.contains(.update) { return .update } // Announcement: lowest priority if updater.announcementAvailable && !dismissedOverlays.contains(.announcement) { return .announcement } return nil } private var shouldShowOverlay: Bool { badgeOverlaysEnabled && currentOverlay != nil } var body: some View { if shouldShowOverlay { VStack { Spacer() HStack { Spacer() VStack(alignment: .leading, spacing: 12) { // Header + Content determined by enum switch if let overlay = currentOverlay { switch overlay { case .helper: helperContent case .permissions: permissionsContent case .update: updateContent case .announcement: announcementContent } } } .padding(10) .frame(width: 410) .ifGlassSidebar() // .background( // RoundedRectangle(cornerRadius: 12) // .fill(ThemeColors.shared(for: colorScheme).secondaryBG) // .shadow(color: .black.opacity(0.3), radius: 15, x: 0, y: 5) // ) .padding(.trailing, 20) .padding(.bottom, 20) .opacity(isVisible ? 1 : 0) .offset(y: isVisible ? 0 : 20) } } .onAppear { withAnimation(animationEnabled ? .spring(response: 0.4, dampingFraction: 0.8) : .none) { isVisible = true } // Listen for helper required notifications to re-show helper overlay NotificationCenter.default.addObserver( forName: .helperRequired, object: nil, queue: .main ) { [self] _ in dismissedOverlays.remove(.helper) } } .onDisappear { NotificationCenter.default.removeObserver(self, name: .helperRequired, object: nil) } .onChange(of: currentOverlay) { newValue in // Animate when switching between overlay types or showing/hiding withAnimation(animationEnabled ? .spring(response: 0.4, dampingFraction: 0.8) : .none) { isVisible = newValue != nil } // Reset expansion when switching between overlay types if newValue != nil { isExpanded = false } } } } // MARK: - Helper Content @ViewBuilder private var helperContent: some View { HStack(alignment: .top) { Image(systemName: "exclamationmark.triangle.fill") .foregroundColor(.orange) .font(.title3) VStack(alignment: .leading, spacing: 8) { Text("Privileged Helper Not Installed!") .font(.headline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) // Expanded content if isExpanded { VStack(alignment: .leading, spacing: 8) { Divider() Text("When the helper tool was introduced March 2025, it was said that AuthorizationExecuteWithPrivileges (granting authentication via password prompt popup) would eventually be removed as it has already been deprecated by Apple. Some functionality will stop working in Pearcleaner if the helper isn't enabled going forward.") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .fixedSize(horizontal: false, vertical: true) Text("Some of the features that require the helper:") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) VStack(alignment: .leading, spacing: 4) { HStack(alignment: .top, spacing: 6) { Text(verbatim: "•") Text("Delete system-protected files and folders") } HStack(alignment: .top, spacing: 6) { Text(verbatim: "•") Text("Unload launch daemons and agents") } HStack(alignment: .top, spacing: 6) { Text(verbatim: "•") Text("Manage PKG receipts and installations") } HStack(alignment: .top, spacing: 6) { Text(verbatim: "•") Text("Perform updates on 3rd party apps, and more") } } .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .transition(.opacity.combined(with: .move(edge: .bottom))) } } Spacer() Button { dismissOverlay() } label: { Image(systemName: "xmark.circle.fill") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .font(.title3) } .buttonStyle(.plain) } Divider() HStack { Button { openAppSettingsWindow(tab: .helper, updater: updater) } label: { HStack { Image(systemName: "gear") Text("Enable Helper") } .padding(.horizontal, 12) .padding(.vertical, 6) .background(ThemeColors.shared(for: colorScheme).accent) .foregroundColor(.white) .cornerRadius(6) } .buttonStyle(.plain) .help("Open Settings to enable the Privileged Helper") Spacer() Button { withAnimation(.easeInOut(duration: 0.2)) { isExpanded.toggle() } } label: { HStack(spacing: 4) { Text(isExpanded ? "See less" : "See more") Image(systemName: isExpanded ? "chevron.up" : "chevron.down") } .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) .contentShape(Rectangle()) } .buttonStyle(.plain) } } // MARK: - Permissions Content @ViewBuilder private var permissionsContent: some View { HStack(alignment: .top) { Image(systemName: "lock.slash.fill") .foregroundColor(.red) .font(.title3) VStack(alignment: .leading, spacing: 8) { Text("Permissions Missing!") .font(.headline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) if isExpanded { VStack(alignment: .leading, spacing: 8) { Divider() Text("Pearcleaner requires permissions to search all system locations comprehensively.") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .fixedSize(horizontal: false, vertical: true) if let results = permissionManager.results { Text("Missing permissions:") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) ForEach(results.checkedPermissions, id: \.self) { permission in if !results.grantedPermissions.contains(permission) { HStack(spacing: 6) { Image(systemName: "xmark.circle.fill") .foregroundColor(.red) .font(.caption) Text(permissionName(for: permission)) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } } } } .transition(.opacity.combined(with: .move(edge: .bottom))) } } Spacer() Button { dismissOverlay() } label: { Image(systemName: "xmark.circle.fill") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .font(.title3) } .buttonStyle(.plain) } Divider() HStack { Button { if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security") { NSWorkspace.shared.open(url) } } label: { HStack { Image(systemName: "gear") Text("Open System Settings") } .padding(.horizontal, 12) .padding(.vertical, 6) .background(ThemeColors.shared(for: colorScheme).accent) .foregroundColor(.white) .cornerRadius(6) } .buttonStyle(.plain) .help("Open System Settings to grant permissions") Button { permissionManager.checkPermissions(types: [.fullDiskAccess]) { results in permissionManager.results = results } } label: { HStack { Image(systemName: "arrow.clockwise") Text("Retry") } .padding(.horizontal, 12) .padding(.vertical, 6) .background(ThemeColors.shared(for: colorScheme).secondaryText) .foregroundColor(.white) .cornerRadius(6) } .buttonStyle(.plain) .help("Retry permission check") Spacer() Button { withAnimation(.easeInOut(duration: 0.2)) { isExpanded.toggle() } } label: { HStack(spacing: 4) { Text(isExpanded ? "See less" : "See more") Image(systemName: isExpanded ? "chevron.up" : "chevron.down") } .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) .contentShape(Rectangle()) } .buttonStyle(.plain) } } // MARK: - Update Content @ViewBuilder private var updateContent: some View { HStack(alignment: .top) { Image(systemName: "icloud.and.arrow.down.fill") .foregroundColor(.green) .font(.title3) VStack(alignment: .leading, spacing: 8) { Text("Update Available!") .font(.headline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) if isExpanded { VStack(alignment: .leading, spacing: 8) { Divider() Text("A new version of Pearcleaner is available for download.") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .fixedSize(horizontal: false, vertical: true) } .transition(.opacity.combined(with: .move(edge: .bottom))) } } Spacer() Button { dismissOverlay() } label: { Image(systemName: "xmark.circle.fill") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .font(.title3) } .buttonStyle(.plain) } Divider() HStack { Button { showUpdateView.toggle() } label: { HStack { Image(systemName: "arrow.down.circle") Text("View Update") } .padding(.horizontal, 12) .padding(.vertical, 6) .background(ThemeColors.shared(for: colorScheme).accent) .foregroundColor(.white) .cornerRadius(6) } .buttonStyle(.plain) .help("View update details and download") .sheet(isPresented: $showUpdateView) { updater.getUpdateView() } Spacer() Button { withAnimation(.easeInOut(duration: 0.2)) { isExpanded.toggle() } } label: { HStack(spacing: 4) { Text(isExpanded ? "See less" : "See more") Image(systemName: isExpanded ? "chevron.up" : "chevron.down") } .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) .contentShape(Rectangle()) } .buttonStyle(.plain) } } // MARK: - Announcement Content @ViewBuilder private var announcementContent: some View { HStack(alignment: .top) { Image(systemName: "sparkles") .foregroundColor(.purple) .font(.title3) VStack(alignment: .leading, spacing: 8) { Text("New Feature!") .font(.headline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) if isExpanded { VStack(alignment: .leading, spacing: 8) { Divider() Text("Check out the latest additions to Pearcleaner.") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .fixedSize(horizontal: false, vertical: true) } .transition(.opacity.combined(with: .move(edge: .bottom))) } } Spacer() Button { dismissOverlay() } label: { Image(systemName: "xmark.circle.fill") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .font(.title3) } .buttonStyle(.plain) } Divider() HStack { Button { showFeatureView.toggle() } label: { HStack { Image(systemName: "sparkles") Text("Learn More") } .padding(.horizontal, 12) .padding(.vertical, 6) .background(ThemeColors.shared(for: colorScheme).accent) .foregroundColor(.white) .cornerRadius(6) } .buttonStyle(.plain) .help("View announcement details") .sheet(isPresented: $showFeatureView) { updater.getAnnouncementView() } Spacer() Button { withAnimation(.easeInOut(duration: 0.2)) { isExpanded.toggle() } } label: { HStack(spacing: 4) { Text(isExpanded ? "See less" : "See more") Image(systemName: isExpanded ? "chevron.up" : "chevron.down") } .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) .contentShape(Rectangle()) } .buttonStyle(.plain) } } // MARK: - Helper Functions private func dismissOverlay() { withAnimation(animationEnabled ? .easeOut(duration: 0.2) : .none) { isVisible = false } DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { guard let overlay = currentOverlay else { return } // Add any dismissed overlay to the set (dismissed for this session) // Helper can be re-shown by removing from set via notification dismissedOverlays.insert(overlay) } } private func permissionName(for permission: PermissionManagerLocal.PermissionType) -> String { switch permission { case .fullDiskAccess: return "Full Disk Access".localized() } } } ================================================ FILE: Pearcleaner/Views/Components/GlobalConsoleView.swift ================================================ // // GlobalConsoleView.swift // Pearcleaner // // Created by Alin Lupascu on 11/13/24. // import SwiftUI struct GlobalConsoleView: View { let output: String @Binding var height: Double let onClear: () -> Void @Environment(\.colorScheme) var colorScheme // @State private var cursorState: CursorState = .normal // @State private var isHovering: Bool = false // enum CursorState { // case normal // case hovering // case dragging // // var cursor: NSCursor { // switch self { // case .normal: return .arrow // case .hovering: return .openHand // case .dragging: return .closedHand // } // } // // func apply() { // cursor.set() // } // } private var shouldShowLineCount: Bool { let trimmedOutput = output.trimmingCharacters(in: .whitespacesAndNewlines) return !trimmedOutput.isEmpty && trimmedOutput != "Ready.." } private var lineCountText: String { // Trim trailing newlines before counting to avoid counting empty trailing lines let trimmedOutput = output.trimmingCharacters(in: .whitespacesAndNewlines) let lineCount = trimmedOutput.components(separatedBy: "\n").count return lineCount == 1 ? "1 line" : "\(lineCount) lines" } var body: some View { VStack(spacing: 0) { // Header with grab handle and buttons ZStack { // Label on left HStack { Text("Console") .font(.system(.caption, design: .monospaced)) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) if shouldShowLineCount { Divider() .frame(height: 10) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text(lineCountText) .font(.system(.caption, design: .monospaced)) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } Spacer() } // Centered resize handle RoundedRectangle(cornerRadius: 2) .fill(ThemeColors.shared(for: colorScheme).secondaryText) .frame(width: 30, height: 2) .padding(6) .contentShape(Rectangle()) .contextMenu { Button("Reset Size") { height = 200 } } // .onHover { hovering in // isHovering = hovering // if cursorState != .dragging { // cursorState = hovering ? .hovering : .normal // cursorState.apply() // } // } .gesture( DragGesture() .onChanged { value in // if cursorState != .dragging { // cursorState = .dragging // cursorState.apply() // } let newHeight = height - value.translation.height height = min(max(newHeight, 150), 400) } // .onEnded { _ in // // Restore cursor based on hover state // cursorState = isHovering ? .hovering : .normal // cursorState.apply() // } ) // Buttons on right HStack { Spacer() Button { NSPasteboard.general.clearContents() NSPasteboard.general.setString(output, forType: .string) } label: { Image(systemName: "clipboard") .resizable() .scaledToFit() .frame(width: 14, height: 15) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .buttonStyle(.plain) .help("Copy console output to clipboard") Button { onClear() } label: { Image(systemName: "trash") .resizable() .scaledToFit() .frame(width: 14, height: 15) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .buttonStyle(.plain) .help("Clear console output") } } .padding(.horizontal, 12) .padding(.vertical, 8) Divider() .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .padding(.horizontal, 8) // Console output ScrollView { ScrollViewReader { proxy in Text(output.isEmpty ? "Ready.." : output) .font(.system(.caption, design: .monospaced)) .foregroundStyle(.green) .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 12) .padding(.vertical, 8) .id("consoleBottom") .textSelection(.enabled) .lineSpacing(1) .onChange(of: output) { _ in withAnimation { proxy.scrollTo("consoleBottom", anchor: .bottom) } } .onChange(of: height) { _ in withAnimation { proxy.scrollTo("consoleBottom", anchor: .bottom) } } .onAppear { withAnimation { proxy.scrollTo("consoleBottom", anchor: .bottom) } } } } } .background(RoundedRectangle(cornerRadius: ifOSBelow(macOS: 26) ? 8 : 20).fill(Color.black)) .shadow(color: Color.black.opacity(1), radius: 10, x: 0, y: 0) .padding(8) } } ================================================ FILE: Pearcleaner/Views/Components/PermissionsSheetView.swift ================================================ // // PermissionsSheetView.swift // Pearcleaner // // Custom permissions view with ThemeColors integration // import SwiftUI import AlinFoundation // MARK: - Local PermissionManager (replaces AlinFoundation version) class PermissionManagerLocal: ObservableObject { static let shared = PermissionManagerLocal() @Published var results: PermissionsCheckResults? private init() {} var allPermissionsGranted: Bool { return results?.fullDiskAccess ?? false } /// Returns true when permission check is complete AND permissions are denied /// Use this for UI warnings/badges to avoid showing false positives before check completes var shouldShowPermissionWarning: Bool { guard let results = results else { return false // Don't show warning until check completes } return results.fullDiskAccess == false } enum PermissionType { case fullDiskAccess } struct PermissionsCheckResults { var fullDiskAccess: Bool? var checkedPermissions: [PermissionType] { return fullDiskAccess != nil ? [.fullDiskAccess] : [] } var grantedPermissions: [PermissionType] { return fullDiskAccess == true ? [.fullDiskAccess] : [] } } func checkPermissions(types: [PermissionType] = [.fullDiskAccess], completion: @escaping (PermissionsCheckResults) -> Void) { checkFullDiskAccess { hasAccess in let results = PermissionsCheckResults(fullDiskAccess: hasAccess) completion(results) } } /// Check Full Disk Access permission with retry logic /// Uses higher priority QoS and faster syscall for better reliability private func checkFullDiskAccess(completion: @escaping (Bool) -> Void) { DispatchQueue.global(qos: .userInitiated).async { // Higher priority than .background let result = self.attemptFDACheck(maxAttempts: 2, delayMs: 100) DispatchQueue.main.async { if !result { printOS("Full Disk Permission: ❌") } completion(result) } } } /// Attempt FDA check with retries and multiple fallback paths /// First tries fast access() syscall, then falls back to directory listing private func attemptFDACheck(maxAttempts: Int, delayMs: Int) -> Bool { // Multiple protected paths to check, in order of preference let checkPaths = [ "~/Library/Containers/com.apple.stocks", // Primary - good for macOS 12+ "~/Library/Safari", // Fallback 1 - more universal "~/Library/Mail" // Fallback 2 - additional option ] for attempt in 1...maxAttempts { // Try each path for checkPath in checkPaths { let expandedPath = NSString(string: checkPath).expandingTildeInPath // Method 1: Try using access() syscall (fastest) if let cPath = expandedPath.cString(using: .utf8) { if access(cPath, R_OK) == 0 { return true // Success - have FDA } } // Method 2: Fallback to directory listing (more reliable but slower) let fileManager = FileManager.default if let _ = try? fileManager.contentsOfDirectory(atPath: expandedPath) { return true } } // If not last attempt, wait before retry if attempt < maxAttempts { Thread.sleep(forTimeInterval: Double(delayMs) / 1000.0) } } return false // All attempts and all paths failed } } // MARK: - PermissionsSheetView struct PermissionsSheetView: View { @Environment(\.dismiss) var dismiss @Environment(\.colorScheme) var colorScheme @ObservedObject var permissionManager = PermissionManagerLocal.shared var body: some View { VStack(alignment: .center, spacing: 10) { // Title HStack { Spacer() Text(LocalizedStringKey("Permission Status")) .font(.title2) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Spacer() } Divider() // Permission list if let results = permissionManager.results { ForEach(results.checkedPermissions, id: \.self) { permission in HStack { // Status icon (keep green/red for universal recognition) Image(systemName: results.grantedPermissions.contains(permission) ? "checkmark.circle.fill" : "xmark.circle.fill") .foregroundColor(results.grantedPermissions.contains(permission) ? .green : .red) // Permission name Text(permissionName(for: permission)) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Spacer() // View button Button { openSettingsForPermission(permission) } label: { Text(LocalizedStringKey("View")) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) } } .padding(5) } } Divider() // Action buttons HStack { Button("Retry") { permissionManager.checkPermissions(types: [.fullDiskAccess]) { results in permissionManager.results = results } } .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Button("Close") { dismiss() } .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } } .padding() .frame(width: 300) .background(ThemeColors.shared(for: colorScheme).primaryBG) .cornerRadius(12) } private func permissionName(for permission: PermissionManagerLocal.PermissionType) -> String { switch permission { case .fullDiskAccess: return "Full Disk Access".localized() } } func openSettingsForPermission(_ permission: PermissionManagerLocal.PermissionType) { let urlString: String switch permission { case .fullDiskAccess: urlString = "x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles" } if let url = URL(string: urlString) { NSWorkspace.shared.open(url) } } } ================================================ FILE: Pearcleaner/Views/Components/SidebarDetailView/GenericSidebarListView.swift ================================================ // // GenericSidebarListView.swift // Pearcleaner // // Created as a reusable component extracted from AppSearchView // import AlinFoundation import SwiftUI struct GenericSidebarListView: View { // Data let items: [Item] let categories: [(title: String, filter: (Item) -> Bool, initiallyExpanded: Bool, isScanning: Bool)] // Bindings @Binding var searchText: String // Customization let searchFilter: ((Item, String) -> Bool)? let emptyMessage: String let noResultsMessage: String let isLoading: Bool let loadingMessage: String @ViewBuilder let itemView: (Item) -> Content // Internal state @Environment(\.colorScheme) var colorScheme @AppStorage("settings.general.sidebarWidthGeneric") private var sidebarWidth: Double = 265 @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true @State private var dimensionStart: Double? init( items: [Item], categories: [(title: String, filter: (Item) -> Bool, initiallyExpanded: Bool, isScanning: Bool)], searchText: Binding, searchFilter: ((Item, String) -> Bool)? = nil, emptyMessage: String = "No items found", noResultsMessage: String = "No results", isLoading: Bool = false, loadingMessage: String = "Loading...", @ViewBuilder itemView: @escaping (Item) -> Content ) { self.items = items self.categories = categories self._searchText = searchText self.searchFilter = searchFilter self.emptyMessage = emptyMessage self.noResultsMessage = noResultsMessage self.isLoading = isLoading self.loadingMessage = loadingMessage self.itemView = itemView } var body: some View { VStack(alignment: .center, spacing: 0) { if items.isEmpty { VStack(spacing: 12) { Spacer() if isLoading { ProgressView() .controlSize(.regular) Text(loadingMessage) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .font(.callout) } else { Text(emptyMessage) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .font(.callout) } Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) } else { SearchBarSidebar(search: $searchText, menu: false) .padding() .padding(.top, 20) CategorizedListView( categories: categorizedItems, itemView: itemView ) .padding([.bottom, .horizontal], 5) } } .frame(width: sidebarWidth) .ifGlassMain() .padding([.leading, .vertical], 8) .ignoresSafeArea(edges: .top) .overlay(alignment: .trailing) { // Invisible resize handle on the trailing edge Rectangle() .fill(Color.clear) .frame(width: 10) .contentShape(Rectangle()) .offset(x: 5) // Center on the edge .onHover { inside in if inside { NSCursor.resizeLeftRight.push() } else { NSCursor.pop() } } .contextMenu { Button("Reset Size") { sidebarWidth = 265 } } .gesture(sidebarDragGesture) .help("Right click to reset size") } } private var sidebarDragGesture: some Gesture { DragGesture(minimumDistance: 5, coordinateSpace: .global) .onChanged { val in if dimensionStart == nil { dimensionStart = sidebarWidth } let delta = val.location.x - val.startLocation.x let newDimension = dimensionStart! + Double(delta) // Standard range for sidebar width let minWidth: Double = 220 let maxWidth: Double = 350 let newWidth = max(minWidth, min(maxWidth, newDimension)) sidebarWidth = newWidth NSCursor.closedHand.set() } .onEnded { val in dimensionStart = nil NSCursor.arrow.set() } } private var filteredItems: [Item] { if searchText.isEmpty { return items } else { // Use custom searchFilter if provided if let customFilter = searchFilter { return items.filter { customFilter($0, searchText) } } // Otherwise, check if items conform to FuzzySearchable and use fuzzy matching else if let fuzzySearchableItems = items as? [any FuzzySearchable] { return fuzzySearchableItems.filter { item in item.fuzzyMatch(query: searchText).weight > 0 } as? [Item] ?? [] } // Fallback: no filtering (return empty to show "no results") else { return [] } } } private var categorizedItems: [(title: String, items: [Item], initiallyExpanded: Bool, isScanning: Bool)] { return categories.map { category in let categoryItems = filteredItems.filter(category.filter) return (category.title, categoryItems, category.initiallyExpanded, category.isScanning) } } } // MARK: - Categorized List View struct CategorizedListView: View { let categories: [(title: String, items: [Item], initiallyExpanded: Bool, isScanning: Bool)] @ViewBuilder let itemView: (Item) -> Content @AppStorage("settings.interface.scrollIndicators") private var scrollIndicators: Bool = false @Environment(\.colorScheme) var colorScheme // Split categories: those with items go above divider, empty/Current/Unsupported go below private var categoriesAboveDivider: [(title: String, items: [Item], initiallyExpanded: Bool, isScanning: Bool)] { categories.filter { $0.items.count > 0 && $0.title != "Unsupported" && $0.title != "Current" } } private var categoriesBelowDivider: [(title: String, items: [Item], initiallyExpanded: Bool, isScanning: Bool)] { categories.filter { $0.items.count == 0 || $0.title == "Unsupported" || $0.title == "Current" } } private var shouldShowDivider: Bool { !categoriesAboveDivider.isEmpty && !categoriesBelowDivider.isEmpty } var body: some View { ScrollView { VStack(alignment: .leading, spacing: 0) { // Categories with updates ForEach(Array(categoriesAboveDivider.enumerated()), id: \.offset) { index, category in GenericSectionView( title: category.title, count: category.items.count, items: category.items, initiallyExpanded: category.initiallyExpanded, isScanning: category.isScanning, itemView: itemView ) .padding(.top, index > 0 ? 5 : 0) } // Divider (only if there are categories both above and below) if shouldShowDivider { Divider() .padding(10) } // Empty categories and Unsupported ForEach(Array(categoriesBelowDivider.enumerated()), id: \.offset) { index, category in GenericSectionView( title: category.title, count: category.items.count, items: category.items, initiallyExpanded: category.initiallyExpanded, isScanning: category.isScanning, itemView: itemView ) .padding(.top, (shouldShowDivider && index == 0) ? 0 : 5) } } } .scrollIndicators(scrollIndicators ? .automatic : .never) } } // MARK: - Generic Section View struct GenericSectionView: View { let title: String let count: Int let items: [Item] let initiallyExpanded: Bool let isScanning: Bool @ViewBuilder let itemView: (Item) -> Content @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true @AppStorage("settings.updater.collapsedCategories") private var collapsedCategoriesData: Data = Data() @State private var hasInitialized = false private var collapsedCategories: Set { get { (try? JSONDecoder().decode(Set.self, from: collapsedCategoriesData)) ?? [] } nonmutating set { collapsedCategoriesData = (try? JSONEncoder().encode(newValue)) ?? Data() } } private var showItems: Bool { !collapsedCategories.contains(title) } init(title: String, count: Int, items: [Item], initiallyExpanded: Bool = true, isScanning: Bool = false, @ViewBuilder itemView: @escaping (Item) -> Content) { self.title = title self.count = count self.items = items self.initiallyExpanded = initiallyExpanded self.isScanning = isScanning self.itemView = itemView } var body: some View { VStack(spacing: 0) { HStack(spacing: 6) { Header(title: title, count: count) // Show scanning indicator next to count if isScanning { ProgressView() .controlSize(.small) .scaleEffect(0.7) } } .padding(.horizontal, 5) .onTapGesture { withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { toggleCategory() } } if showItems { ForEach(items) { item in itemView(item) .transition(.opacity) } } } .onAppear { initializeCollapseState() } } private func initializeCollapseState() { guard !hasInitialized else { return } hasInitialized = true // If this category should default to collapsed and isn't in the set yet, add it if !initiallyExpanded && !collapsedCategories.contains(title) { var categories = collapsedCategories categories.insert(title) collapsedCategories = categories } } private func toggleCategory() { var categories = collapsedCategories if categories.contains(title) { // Was collapsed, now expand it categories.remove(title) } else { // Was expanded, now collapse it categories.insert(title) } collapsedCategories = categories } } ================================================ FILE: Pearcleaner/Views/Components/SidebarDetailView/SidebarDetailLayout.swift ================================================ // // SidebarDetailLayout.swift // Pearcleaner // // Created as a reusable layout component extracted from MainWindow applicationsView // import SwiftUI struct SidebarDetailLayout: View { @ViewBuilder let sidebar: () -> Sidebar @ViewBuilder let detail: () -> Detail var body: some View { HStack(alignment: .center, spacing: 0) { // Sidebar content sidebar() .transition(.opacity) // Detail content HStack(spacing: 0) { Group { detail() } .transition(.opacity) .frame(maxWidth: .infinity, maxHeight: .infinity) } .zIndex(2) } } } ================================================ FILE: Pearcleaner/Views/Components/StandardSheetView.swift ================================================ // // StandardSheetView.swift // Pearcleaner // // Created by Alin Lupascu on 10/26/25. // import SwiftUI struct StandardSheetView: View { let title: String let width: CGFloat let height: CGFloat let onClose: () -> Void let content: () -> Content let selectionControls: () -> SelectionControls let actionButtons: () -> ActionButtons @Environment(\.colorScheme) var colorScheme init( title: String, width: CGFloat = 600, height: CGFloat = 500, onClose: @escaping () -> Void, @ViewBuilder content: @escaping () -> Content, @ViewBuilder selectionControls: @escaping () -> SelectionControls = { EmptyView() }, @ViewBuilder actionButtons: @escaping () -> ActionButtons ) { self.title = title self.width = width self.height = height self.onClose = onClose self.content = content self.selectionControls = selectionControls self.actionButtons = actionButtons } var body: some View { VStack(spacing: 0) { // Header HStack { Text(title) .font(.title2) .fontWeight(.bold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Spacer() Button { onClose() } label: { Image(systemName: "xmark.circle.fill") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .buttonStyle(.plain) } .padding(20) Divider() // Content area with spacing VStack(spacing: 20) { content() Spacer() } .padding(.horizontal, 20) .padding(.top, 20) Divider() // Bottom toolbar HStack { if SelectionControls.self != EmptyView.self { // Selection controls on left selectionControls() Spacer() // Action buttons on right actionButtons() } else { // No selection controls - center action buttons Spacer() actionButtons() Spacer() } } .padding(20) } .frame(width: width, height: height) .background(ThemeColors.shared(for: colorScheme).primaryBG) } } ================================================ FILE: Pearcleaner/Views/Components/TCCPermissionViewer.swift ================================================ // // TCCPermissionViewer.swift // Pearcleaner // // Sheet view for displaying TCC (privacy) permissions for an application // import SwiftUI struct TCCPermissionViewer: View { let bundleIdentifier: String let appName: String let onClose: () -> Void @Environment(\.colorScheme) var colorScheme @State private var isLoading: Bool = true @State private var result: TCCQueryResult = TCCQueryResult() var body: some View { StandardSheetView( title: "Permissions for \(appName)", width: 700, height: 500, onClose: onClose, content: { // Content area - no tabs if isLoading { loadingView } else if !result.hasAnyPermissions { emptyStateView(message: "No permissions found") } else { permissionListView(permissions: result.allPermissions) } }, actionButtons: { HStack(spacing: 12) { Button("Refresh") { loadPermissions() } .buttonStyle(.bordered) .disabled(isLoading) Button("Close") { onClose() } .buttonStyle(.borderedProminent) } } ) .onAppear { loadPermissions() } } // MARK: - Loading View private var loadingView: some View { VStack { Spacer() ProgressView() .scaleEffect(1.2) Text("Loading permissions...") .font(.body) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .padding(.top, 12) Spacer() } } // MARK: - Permission List View private func permissionListView( permissions: [TCCPermission] ) -> some View { ScrollView { VStack(alignment: .leading, spacing: 0) { ForEach(permissions) { permission in permissionRow(permission) if permission.id != permissions.last?.id { Divider() .padding(.leading, 16) } } } .padding(.vertical, 8) } } // MARK: - Permission Row private func permissionRow(_ permission: TCCPermission) -> some View { VStack(alignment: .leading, spacing: 8) { HStack { // Permission name Text(permission.displayName) .font(.body) .fontWeight(.medium) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) // Source badge (USER or SYSTEM) Text(permission.source.rawValue) .font(.caption2) .fontWeight(.semibold) .padding(.horizontal, 6) .padding(.vertical, 2) .background(permission.sourceColor.opacity(0.15)) .foregroundStyle(permission.sourceColor) .cornerRadius(4) Spacer() // Status badge Text(permission.statusText) .font(.caption) .padding(.horizontal, 8) .padding(.vertical, 4) .background(permission.statusColor.opacity(0.2)) .foregroundStyle(permission.statusColor) .cornerRadius(6) } // Optional details if permission.reasonText != nil || permission.lastModified != nil { VStack(alignment: .leading, spacing: 4) { if let reasonText = permission.reasonText { Text("Previous action: \(reasonText)") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } if let date = permission.lastModified { Text("Last modified: \(formattedDate(date))") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } } } .padding(.horizontal, 16) .padding(.vertical, 12) } // MARK: - Empty State private func emptyStateView(message: String) -> some View { VStack(spacing: 12) { Spacer() Image(systemName: "checkmark.shield") .font(.system(size: 48)) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText.opacity(0.5)) Text(message) .font(.body) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) } // MARK: - Load Permissions private func loadPermissions() { isLoading = true Task { let queryResult = await TCCQueryHelper.queryAllDatabases( bundleIdentifier: bundleIdentifier ) await MainActor.run { self.result = queryResult self.isLoading = false } } } // MARK: - Formatting private func formattedDate(_ date: Date) -> String { let formatter = DateFormatter() formatter.dateStyle = .medium formatter.timeStyle = .short return formatter.string(from: date) } } ================================================ FILE: Pearcleaner/Views/DaemonView.swift ================================================ // // DaemonView.swift // Pearcleaner // // Created by Alin Lupascu on 8/10/25. // import SwiftUI import AlinFoundation struct LaunchItem: Identifiable, Hashable, Equatable { let id = UUID() let label: String let path: String let domain: String // user, system, gui, etc. let status: String // loaded, unloaded, error let pid: String? let type: LaunchItemType let bundlePath: String? let embeddedPlistPaths: [String] // Additional embedded plist locations enum LaunchItemType: String, CaseIterable { case agent = "Launch Agent" case daemon = "Launch Daemon" case service = "XPC Service" var systemImage: String { switch self { case .agent: return "person.circle" case .daemon: return "gear.circle" case .service: return "network.circle" } } } } struct DaemonView: View { @EnvironmentObject var appState: AppState @Environment(\.colorScheme) var colorScheme @ObservedObject private var consoleManager = GlobalConsoleManager.shared @State private var launchItems: [LaunchItem] = [] @State private var isLoading: Bool = false @State private var lastRefreshDate: Date? @State private var selectedFilter: LaunchItemFilter = .all @State private var searchText: String = "" @AppStorage("settings.interface.scrollIndicators") private var scrollIndicators: Bool = false enum LaunchItemFilter: String, CaseIterable { case all = "All" case loaded = "Loaded" case unloaded = "Unloaded" case running = "Running" case agents = "Launch Agents" case daemons = "Launch Daemons" case services = "XPC Services" var systemImage: String { switch self { case .all: return "list.bullet" case .loaded: return "checkmark.circle" case .unloaded: return "xmark.circle" case .running: return "play.circle" case .agents: return "person.circle" case .daemons: return "gear.circle" case .services: return "network" } } } private var filteredItems: [LaunchItem] { var items = launchItems.filter { !$0.label.hasPrefix("com.apple.") } // Apply search filter if !searchText.isEmpty { items = items.filter { item in item.label.localizedCaseInsensitiveContains(searchText) || item.path.localizedCaseInsensitiveContains(searchText) || (item.bundlePath?.localizedCaseInsensitiveContains(searchText) ?? false) } } // Apply type/status filter switch selectedFilter { case .all: return items case .loaded: return items.filter { isItemLoaded($0) } case .unloaded: return items.filter { !isItemLoaded($0) } case .running: return items.filter { isItemRunning($0) } case .agents: return items.filter { $0.type == .agent } case .daemons: return items.filter { $0.type == .daemon } case .services: return items.filter { $0.type == .service } } } private func isItemLoaded(_ item: LaunchItem) -> Bool { // Simple logic: if status is "Not Loaded", it's not loaded // If it has any other status, it was found in launchctl list and is loaded return item.status != "Not Loaded" } private func isItemRunning(_ item: LaunchItem) -> Bool { // An item is running if it's loaded AND has a PID (same logic as friendlyStatus) if item.status == "Not Loaded" { return false } if let statusCode = Int(item.status), statusCode == 0 { return item.pid != nil && item.pid != "-" && !item.pid!.isEmpty } return false } private func friendlyStatus(for item: LaunchItem) -> String { // Handle our custom "Not Loaded" status first if item.status == "Not Loaded" { return "Not Loaded" } // Check if it's a numeric status code if let statusCode = Int(item.status) { switch statusCode { case 0: // If it has a PID, it's running, otherwise it's just loaded but not active if item.pid != nil && item.pid != "-" && !item.pid!.isEmpty { return "Running" } else { return "Loaded" } case -1: return "Error (Exit -1)" case -2: return "Error (Exit -2)" case -3: return "Error (Exit -3)" default: if statusCode > 0 { return "Error (Exit \(statusCode))" } else { return "Error (\(statusCode))" } } } // Handle text status (shouldn't happen with new logic, but just in case) return item.status.capitalized } var body: some View { VStack(alignment: .leading, spacing: 0) { // Search bar HStack { Image(systemName: "magnifyingglass") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) TextField("Search...", text: $searchText) .textFieldStyle(.plain) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) if !searchText.isEmpty { Button { searchText = "" } label: { Image(systemName: "xmark.circle.fill") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .buttonStyle(.plain) } } .padding(.horizontal, 12) .padding(.vertical, 8) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) .controlGroup(Capsule(style: .continuous), level: .primary) .padding(.top, 5) if isLoading { VStack(alignment: .center, spacing: 10) { Spacer() ProgressView() .scaleEffect(1.5) Text("Loading launch services...") .font(.title2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() } .frame(maxWidth: .infinity) } else if filteredItems.isEmpty { VStack(alignment: .center) { Spacer() Image(systemName: selectedFilter.systemImage) .font(.system(size: 48)) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text(launchItems.isEmpty ? "No launch services found" : "No items match the current filters") .font(.title2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) if !searchText.isEmpty { Text("Try adjusting your search or filters") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } Spacer() } .frame(maxWidth: .infinity) } else { // Stats header HStack { Text("\(filteredItems.count) service\(filteredItems.count == 1 ? "" : "s")") .font(.caption) .monospacedDigit() .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) if selectedFilter == .all && !launchItems.isEmpty { let loadedCount = filteredItems.filter { isItemLoaded($0) }.count let unloadedCount = filteredItems.count - loadedCount Text(verbatim: "•") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text("\(loadedCount) loaded") .font(.caption) .monospacedDigit() .foregroundStyle(.blue) Text(verbatim: "•") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text("\(unloadedCount) not loaded") .font(.caption) .monospacedDigit() .foregroundStyle(.orange) } Spacer() if let lastRefresh = lastRefreshDate { TimelineView(.periodic(from: lastRefresh, by: 1.0)) { _ in Text("Updated \(formatRelativeTime(lastRefresh))") .font(.caption) .monospacedDigit() .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } } .padding(.vertical) ScrollView { LazyVStack(spacing: 8) { ForEach(filteredItems, id: \.id) { item in LaunchItemRowView(item: item) { refreshLaunchItems() } } } } .scrollIndicators(scrollIndicators ? .automatic : .never) } // Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding([.horizontal], 20) .onAppear { if launchItems.isEmpty { refreshLaunchItems() } } .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("DaemonViewShouldRefresh"))) { _ in refreshLaunchItems() } .toolbarBackground(.hidden, for: .windowToolbar) .toolbar { TahoeToolbarItem(placement: .navigation) { VStack(alignment: .leading) { Text("Launch Services Manager") .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .font(.title2) .fontWeight(.bold) Text("Manage launch agents, daemons, and XPC services") .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } ToolbarItem { Spacer() } TahoeToolbarItem(isGroup: true) { Button { withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) { consoleManager.showConsole.toggle() } } label: { Label("Console", systemImage: consoleManager.showConsole ? "terminal.fill" : "terminal") } .help("Toggle console output") Menu { ForEach(LaunchItemFilter.allCases, id: \.self) { filter in Button { selectedFilter = filter } label: { Label(filter.rawValue, systemImage: filter.systemImage) } } } label: { Label(selectedFilter.rawValue, systemImage: selectedFilter.systemImage) } .labelStyle(.titleAndIcon) .menuIndicator(.hidden) Button { refreshLaunchItems() } label: { Label("Refresh", systemImage: "arrow.counterclockwise") } .disabled(isLoading) } } } private func refreshLaunchItems() { GlobalConsoleManager.shared.appendOutput("Refreshing launch services...\n", source: CurrentPage.services.title) isLoading = true Task { let items = await loadLaunchItems() await MainActor.run { self.launchItems = items.sorted { first, second in return first.label.localizedCaseInsensitiveCompare(second.label) == .orderedAscending } self.lastRefreshDate = Date() self.isLoading = false GlobalConsoleManager.shared.appendOutput("✓ Loaded \(items.count) launch services\n", source: CurrentPage.services.title) } } } private func loadLaunchItems() async -> [LaunchItem] { return await withCheckedContinuation { continuation in DispatchQueue.global(qos: .userInitiated).async { var items: [LaunchItem] = [] // First, get runtime status from launchctl list let runtimeStatus = self.getRuntimeStatus() // First pass: collect all embedded plist locations let embeddedPlists = self.collectEmbeddedPlists() // Scan directories for plist files in priority order items.append(contentsOf: self.scanLaunchAgents(runtimeStatus: runtimeStatus, embeddedPlists: embeddedPlists)) items.append(contentsOf: self.scanLaunchDaemons(runtimeStatus: runtimeStatus, embeddedPlists: embeddedPlists)) // Scan app bundles, but skip any labels we've already found items.append(contentsOf: self.scanAppBundles(runtimeStatus: runtimeStatus, existingItems: items)) // Add any services found in launchctl but missing plist files items.append(contentsOf: self.addMissingRuntimeServices(runtimeStatus: runtimeStatus, existingItems: items)) continuation.resume(returning: items) } } } private func collectEmbeddedPlists() -> [String: [String]] { var embeddedPlists: [String: [String]] = [:] let fileManager = FileManager.default let appPaths = [ "/Applications", "/System/Applications", "/System/Applications/Utilities", "~/Applications" ] for basePath in appPaths { let expandedPath = NSString(string: basePath).expandingTildeInPath guard let apps = try? fileManager.contentsOfDirectory(atPath: expandedPath) else { continue } for app in apps where app.hasSuffix(".app") { let appPath = "\(expandedPath)/\(app)" let possiblePaths = [ "\(appPath)/Contents/Library/LaunchAgents", "\(appPath)/Contents/Library/LaunchDaemons", "\(appPath)/Contents/Resources/LaunchAgents", "\(appPath)/Contents/Resources/LaunchDaemons", "\(appPath)/Contents/XPCServices", "\(appPath)/Contents/Helpers", "\(appPath)/Contents/MacOS" ] for plistPath in possiblePaths { guard let plistFiles = try? fileManager.contentsOfDirectory(atPath: plistPath) else { continue } for filename in plistFiles where filename.hasSuffix(".plist") { let fullPath = "\(plistPath)/\(filename)" let actualLabel = readLabelFromPlist(path: fullPath) let fallbackLabel = String(filename.dropLast(6)) let label = actualLabel ?? fallbackLabel if embeddedPlists[label] == nil { embeddedPlists[label] = [] } embeddedPlists[label]?.append(fullPath) } } } } return embeddedPlists } private func scanLaunchAgents(runtimeStatus: [String: (pid: String?, status: String, isLoaded: Bool)], embeddedPlists: [String: [String]]) -> [LaunchItem] { let agentPaths = [ "~/Library/LaunchAgents", "/Library/LaunchAgents", "/System/Library/LaunchAgents" ] return scanPlistDirectory(paths: agentPaths, type: .agent, runtimeStatus: runtimeStatus, embeddedPlists: embeddedPlists) } private func scanLaunchDaemons(runtimeStatus: [String: (pid: String?, status: String, isLoaded: Bool)], embeddedPlists: [String: [String]]) -> [LaunchItem] { let daemonPaths = [ "/Library/LaunchDaemons", "/System/Library/LaunchDaemons" ] return scanPlistDirectory(paths: daemonPaths, type: .daemon, runtimeStatus: runtimeStatus, embeddedPlists: embeddedPlists) } private func scanPlistDirectory(paths: [String], type: LaunchItem.LaunchItemType, runtimeStatus: [String: (pid: String?, status: String, isLoaded: Bool)], embeddedPlists: [String: [String]]) -> [LaunchItem] { var items: [LaunchItem] = [] let fileManager = FileManager.default for path in paths { let expandedPath = NSString(string: path).expandingTildeInPath guard let contents = try? fileManager.contentsOfDirectory(atPath: expandedPath) else { continue } for filename in contents where filename.hasSuffix(".plist") { let fullPath = "\(expandedPath)/\(filename)" // Read the actual Label from the plist file instead of using filename let actualLabel = readLabelFromPlist(path: fullPath) let fallbackLabel = String(filename.dropLast(6)) let label = actualLabel ?? fallbackLabel // Get runtime status let runtime = runtimeStatus[label] let isLoaded = runtime?.isLoaded ?? false let status = runtime?.status ?? "0" let pid = runtime?.pid // Determine domain let domain = determineDomainFromPath(expandedPath) // Create custom status based on loaded state let displayStatus = isLoaded ? status : "Not Loaded" // Get embedded plist paths for this label let embeddedPaths = embeddedPlists[label] ?? [] let item = LaunchItem( label: label, path: fullPath, domain: domain, status: displayStatus, pid: pid, type: type, bundlePath: nil, embeddedPlistPaths: embeddedPaths ) items.append(item) } } return items } private func readLabelFromPlist(path: String) -> String? { guard let plistData = try? Data(contentsOf: URL(fileURLWithPath: path)), let plist = try? PropertyListSerialization.propertyList(from: plistData, options: [], format: nil) as? [String: Any], let label = plist["Label"] as? String else { return nil } return label } private func determineDomainFromPath(_ path: String) -> String { if path.contains("/System/") { return "system" } else if path.contains("~/Library/") || path.contains("/Users/") { return "user" } else { return "global" } } private func addMissingRuntimeServices(runtimeStatus: [String: (pid: String?, status: String, isLoaded: Bool)], existingItems: [LaunchItem]) -> [LaunchItem] { var items: [LaunchItem] = [] let existingLabels = Set(existingItems.map { $0.label }) // Find services in runtime status that don't have plist files for (label, runtime) in runtimeStatus { // Skip if we already found this service via plist scanning if existingLabels.contains(label) { continue } // Only include non-Apple services (to match our filtering) if label.hasPrefix("com.apple.") { continue } // Skip system application instances (these are temporary) if label.hasPrefix("application.") { continue } // Determine type based on label patterns let type: LaunchItem.LaunchItemType if label.contains(".helper") || label.contains(".daemon") { type = .daemon } else if label.contains(".xpc") || label.contains("Service") { type = .service } else { type = .agent } // Determine domain - check if it was found in system vs user context let domain = determineRuntimeDomain(for: label, type: type) // Determine path description - all runtime-only services get the same description let pathDescription = "Runtime Only" let item = LaunchItem( label: label, path: pathDescription, domain: domain, status: runtime.status, pid: runtime.pid, type: type, bundlePath: nil, embeddedPlistPaths: [] ) items.append(item) } return items } private func determineRuntimeDomain(for label: String, type: LaunchItem.LaunchItemType) -> String { if label.hasPrefix("com.apple.") { return "system" } else if label.hasPrefix("application.") { return "system" } else if label.hasPrefix("~/Library/") || label.hasPrefix("/Users/") { return "user" } else { return "global" } } private func getRuntimeStatus() -> [String: (pid: String?, status: String, isLoaded: Bool)] { var statusMap: [String: (pid: String?, status: String, isLoaded: Bool)] = [:] // Get user context services let userResult = runDirectShellCommand(command: "launchctl list") if userResult.0 { parseStatusOutput(userResult.1, into: &statusMap, context: "user") } // Get system context services (requires sudo for full access) let systemResult = runDirectShellCommand(command: "sudo launchctl list") if systemResult.0 { parseStatusOutput(systemResult.1, into: &statusMap, context: "system") } return statusMap } private func parseStatusOutput(_ output: String, into statusMap: inout [String: (pid: String?, status: String, isLoaded: Bool)], context: String) { let lines = output.components(separatedBy: .newlines) for line in lines { let trimmed = line.trimmingCharacters(in: .whitespacesAndNewlines) if trimmed.isEmpty || trimmed.hasPrefix("PID") { continue } let components = trimmed.components(separatedBy: .whitespaces).filter { !$0.isEmpty } guard components.count >= 3 else { continue } let pid = components[0] == "-" ? nil : components[0] let status = components[1] let label = components[2] // If it appears in launchctl list, it's loaded (regardless of status code) statusMap[label] = (pid: pid, status: status, isLoaded: true) } } private func scanAppBundles(runtimeStatus: [String: (pid: String?, status: String, isLoaded: Bool)], existingItems: [LaunchItem]) -> [LaunchItem] { var items: [LaunchItem] = [] let fileManager = FileManager.default let existingLabels = Set(existingItems.map { $0.label }) let appPaths = [ "/Applications", "/System/Applications", "/System/Applications/Utilities", "~/Applications" ] for basePath in appPaths { let expandedPath = NSString(string: basePath).expandingTildeInPath guard let apps = try? fileManager.contentsOfDirectory(atPath: expandedPath) else { continue } for app in apps where app.hasSuffix(".app") { let appPath = "\(expandedPath)/\(app)" // Check multiple possible locations within app bundles let possiblePaths = [ "\(appPath)/Contents/Library/LaunchAgents", "\(appPath)/Contents/Library/LaunchDaemons", "\(appPath)/Contents/Resources/LaunchAgents", "\(appPath)/Contents/Resources/LaunchDaemons", "\(appPath)/Contents/XPCServices", "\(appPath)/Contents/Helpers", "\(appPath)/Contents/MacOS" ] for plistPath in possiblePaths { guard let plistFiles = try? fileManager.contentsOfDirectory(atPath: plistPath) else { continue } for filename in plistFiles where filename.hasSuffix(".plist") { let fullPath = "\(plistPath)/\(filename)" // Read the actual Label from the plist file instead of using filename let actualLabel = readLabelFromPlist(path: fullPath) let fallbackLabel = String(filename.dropLast(6)) let label = actualLabel ?? fallbackLabel // Skip this embedded plist if we already have an item with the same label // This prioritizes system-installed plists over embedded ones if existingLabels.contains(label) { continue } let runtime = runtimeStatus[label] let isLoaded = runtime?.isLoaded ?? false let status = runtime?.status ?? "0" let pid = runtime?.pid // Determine type based on path let type: LaunchItem.LaunchItemType if plistPath.contains("LaunchAgents") { type = .agent } else if plistPath.contains("LaunchDaemons") { type = .daemon } else { type = .service } let displayStatus = isLoaded ? status : "Not Loaded" let item = LaunchItem( label: label, path: fullPath, domain: type == .daemon ? "global" : "user", status: displayStatus, pid: pid, type: type, bundlePath: appPath, embeddedPlistPaths: [] ) items.append(item) } } } } return items } } struct LaunchItemRowView: View { @Environment(\.colorScheme) var colorScheme let item: LaunchItem let onUpdate: () -> Void @State private var isPerformingAction = false var body: some View { VStack(alignment: .leading, spacing: 12) { HStack(alignment: .top, spacing: 12) { // Type icon and status indicator VStack(spacing: 4) { ZStack { Circle() .fill(typeColor.opacity(0.2)) .frame(width: 32, height: 32) Image(systemName: item.type.systemImage) .font(.system(size: 14, weight: .medium)) .foregroundStyle(typeColor) } Circle() .fill(statusColor) .frame(width: 8, height: 8) } // Main content VStack(alignment: .leading, spacing: 6) { // Service name and type HStack(alignment: .center) { Text(item.label) .font(.headline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .lineLimit(1) Text(item.type.rawValue) .font(.caption) .padding(.horizontal, 6) .padding(.vertical, 2) .background(typeColor.opacity(0.2)) .foregroundStyle(typeColor) .cornerRadius(4) Spacer() } // Status and PID HStack { HStack(spacing: 4) { Text("Status:") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text(friendlyStatus(for: item)) .font(.caption) .foregroundStyle(statusColor) } if let pid = item.pid { Text(verbatim: "•") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text("PID: \(pid)") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } Spacer() } // Path information if !item.path.isEmpty && item.path != "Not found" { HStack(alignment: .center, spacing: 8) { Button { if let bundlePath = item.bundlePath { NSWorkspace.shared.open(URL(fileURLWithPath: bundlePath)) } else { let path = URL(fileURLWithPath: item.path) NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path) } } label: { Image(systemName: "folder") } .buttonStyle(.plain) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) .help("Open in Finder") VStack(alignment: .leading, spacing: 2) { if let bundlePath = item.bundlePath { Text("Bundle: \(bundlePath)") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .lineLimit(1) .truncationMode(.middle) } Text("Plist: \(item.path)") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .lineLimit(1) .truncationMode(.middle) // Show additional embedded plist locations if they exist if !item.embeddedPlistPaths.isEmpty { ForEach(Array(item.embeddedPlistPaths.enumerated()), id: \.offset) { index, embeddedPath in HStack { Image(systemName: "doc.badge.plus") .font(.caption2) .foregroundStyle(.orange) Text("Also in: \(embeddedPath)") .font(.caption2) .foregroundStyle(.orange) .lineLimit(1) .truncationMode(.middle) } } } } Spacer() } } } // Action buttons VStack(spacing: 6) { HStack(spacing: 6) { if isLoaded { Button("Stop") { performLaunchctlAction("unload", showConfirmation: true) } .buttonStyle(.borderless) .controlSize(.mini) .foregroundStyle(.orange) .disabled(isPerformingAction) .help("Unload the service") Button("Restart") { performLaunchctlAction("kickstart", showConfirmation: false) } .buttonStyle(.borderless) .controlSize(.mini) .foregroundStyle(.blue) .disabled(isPerformingAction) .help("Restart the service") } else { Button("Start") { performLaunchctlAction("load", showConfirmation: false) } .buttonStyle(.borderless) .controlSize(.mini) .foregroundStyle(.green) .disabled(isPerformingAction) .help("Load the service") } Button("Remove") { performLaunchctlAction("remove", showConfirmation: true) } .buttonStyle(.borderless) .controlSize(.mini) .foregroundStyle(.red) .disabled(isPerformingAction) .help("Remove the service registration") } if isPerformingAction { ProgressView() .scaleEffect(0.7) } } } } .padding() .background(ThemeColors.shared(for: colorScheme).secondaryBG.clipShape(RoundedRectangle(cornerRadius: 8))) } private var isLoaded: Bool { // Simple logic: if status is "Not Loaded", it's not loaded // If it has any other status, it was found in launchctl list and is loaded return item.status != "Not Loaded" } private var statusColor: Color { if item.status.lowercased().contains("error") || (item.status != "Not Loaded" && item.status != "0" && Int(item.status) != nil && Int(item.status)! != 0) { return .red } else if item.pid != nil && item.pid != "-" && !item.pid!.isEmpty { return .green // Actually running (has PID) } else if isLoaded { return .blue // Loaded but not running } else { return .orange // Not loaded } } private var typeColor: Color { switch item.type { case .agent: return .blue case .daemon: return .purple case .service: return .teal } } private func friendlyStatus(for item: LaunchItem) -> String { // Handle our custom "Not Loaded" status first if item.status == "Not Loaded" { return "Not Loaded" } // Check if it's a numeric status code if let statusCode = Int(item.status) { switch statusCode { case 0: // If it has a PID, it's running, otherwise it's just loaded but not active if item.pid != nil && item.pid != "-" && !item.pid!.isEmpty { return "Running" } else { return "Loaded" } case -1: return "Error (Exit -1)" case -2: return "Error (Exit -2)" case -3: return "Error (Exit -3)" default: if statusCode > 0 { return "Error (Exit \(statusCode))" } else { return "Error (\(statusCode))" } } } // Handle text status (shouldn't happen with new logic, but just in case) return item.status.capitalized } private func performLaunchctlAction(_ action: String, showConfirmation: Bool) { let actionText = action.capitalized let message = "Are you sure you want to \(action) '\(item.label)'?" if showConfirmation { showCustomAlert( title: "Confirm \(actionText)", message: message, style: .warning, onOk: { executeLaunchctlCommand(action) } ) } else { executeLaunchctlCommand(action) } } private func executeLaunchctlCommand(_ action: String) { isPerformingAction = true Task { let needsSudo = isDaemonOrSystemService() let command: String let domain = item.domain == "user" ? "gui/\(getuid())" : "system" // Build the appropriate command based on action and privilege needs switch action { case "load": if !item.path.isEmpty && item.path != "Not found" { command = needsSudo ? "sudo launchctl load '\(item.path)'" : "launchctl load '\(item.path)'" } else { command = needsSudo ? "sudo launchctl enable \(domain)/\(item.label)" : "launchctl enable \(domain)/\(item.label)" } case "unload": if !item.path.isEmpty && item.path != "Not found" { command = needsSudo ? "sudo launchctl unload '\(item.path)'" : "launchctl unload '\(item.path)'" } else { command = needsSudo ? "sudo launchctl disable \(domain)/\(item.label)" : "launchctl disable \(domain)/\(item.label)" } case "kickstart": command = needsSudo ? "sudo launchctl kickstart -k \(domain)/\(item.label)" : "launchctl kickstart -k \(domain)/\(item.label)" case "remove": command = needsSudo ? "sudo launchctl remove \(item.label)" : "launchctl remove \(item.label)" default: await MainActor.run { isPerformingAction = false } return } var success = false var output = "" // Execute command based on privilege requirements if needsSudo { // Use privileged command execution for daemons/system services let result = try! await runSUCommand( command, errorContext: "Daemon operation failed", throwOnFailure: false ) success = result.0 output = result.1 } else { // Use direct shell command for user agents let result = await Task.detached { return runDirectShellCommand(command: command) }.value success = result.0 output = result.1 if !success { printOS("User launch command failed: \(output)") } } await MainActor.run { isPerformingAction = false if success { printOS("Successfully executed: \(command)") // Success - refresh the list after a short delay DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { onUpdate() } } else { // Show error showCustomAlert( title: "Action Failed", message: "Failed to \(action) '\(item.label)':\n\(output)", style: .critical ) } } } } private func isDaemonOrSystemService() -> Bool { // Check if it's a daemon type if item.type == .daemon { return true } // Check if path contains daemon directories if item.path.contains("/LaunchDaemons/") || item.path.contains("/System/") || item.label.hasPrefix("com.apple.") { return true } // Check if it's a system domain service if item.domain == "system" || item.domain == "global" { return true } return false } } // Helper function to run shell commands (similar to the one in UndoManager.swift) private func runDirectShellCommand(command: String) -> (Bool, String) { let task = Process() task.launchPath = "/bin/sh" task.arguments = ["-c", command] let pipe = Pipe() task.standardOutput = pipe task.standardError = pipe task.launch() task.waitUntilExit() let data = pipe.fileHandleForReading.readDataToEndOfFile() let output = String(data: data, encoding: .utf8) ?? "" return (task.terminationStatus == 0, output) } ================================================ FILE: Pearcleaner/Views/DeleteHistoryView.swift ================================================ // // DeleteHistoryView.swift // Pearcleaner // // Created by Alin Lupascu on 11/10/25. // import SwiftUI import AlinFoundation struct DeleteHistoryView: View { @EnvironmentObject var appState: AppState @EnvironmentObject var locations: Locations @EnvironmentObject var fsm: FolderSettingsManager @StateObject private var historyManager = UndoHistoryManager.shared @Environment(\.colorScheme) var colorScheme @State private var selectedRecords = Set() @State private var isRestoring = false var body: some View { StandardSheetView( title: "Delete History", width: 700, height: 500, onClose: { appState.showDeleteHistory = false }, content: { if historyManager.history.isEmpty { VStack { Spacer() Text("No delete history") .font(.title2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text("Deleted files will appear here") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() } .frame(maxWidth: .infinity) } else { VStack(alignment: .leading, spacing: 10) { // Info text Text("Restore previously deleted files from trash (last 10 operations)") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .padding(.bottom, 5) // List of history records ScrollView { LazyVStack(spacing: 0) { ForEach(Array(historyManager.history.enumerated()), id: \.element.id) { index, record in HistoryRecordRow( record: record, isSelected: selectedRecords.contains(record.id), isValid: historyManager.isRecordValid(record), onToggle: { if selectedRecords.contains(record.id) { selectedRecords.remove(record.id) } else { selectedRecords.insert(record.id) } } ) if index < historyManager.history.count - 1 { Divider() } } } } .background(ThemeColors.shared(for: colorScheme).secondaryBG) .cornerRadius(8) } } }, selectionControls: { if !historyManager.history.isEmpty { Button(selectedRecords.count == historyManager.history.count ? "Deselect All" : "Select All") { if selectedRecords.count == historyManager.history.count { selectedRecords.removeAll() } else { selectedRecords = Set(historyManager.history.map { $0.id }) } } .buttonStyle(ControlGroupButtonStyle( foregroundColor: ThemeColors.shared(for: colorScheme).accent, shape: Capsule(style: .continuous), level: .primary, skipControlGroup: true )) .disabled(isRestoring) } }, actionButtons: { HStack(spacing: 10) { Button("Cancel") { appState.showDeleteHistory = false } .buttonStyle(ControlGroupButtonStyle( foregroundColor: ThemeColors.shared(for: colorScheme).accent, shape: Capsule(style: .continuous), level: .primary, skipControlGroup: true )) .disabled(isRestoring) if !selectedRecords.isEmpty { Divider().frame(height: 10) Button { restoreSelectedRecords() } label: { if isRestoring { HStack(spacing: 8) { ProgressView() .scaleEffect(0.7) Text("Restoring...") } } else { Label("Restore \(selectedRecords.count)", systemImage: "arrow.uturn.backward") } } .buttonStyle(ControlGroupButtonStyle( foregroundColor: ThemeColors.shared(for: colorScheme).accent, shape: Capsule(style: .continuous), level: .primary, skipControlGroup: true )) .disabled(isRestoring) } } .controlGroup(Capsule(style: .continuous), level: .primary) } ) } private func restoreSelectedRecords() { isRestoring = true let recordsToRestore = historyManager.history.filter { selectedRecords.contains($0.id) } Task { do { try await historyManager.restoreRecords(recordsToRestore) selectedRecords.removeAll() isRestoring = false // Refresh app list and file view (same as Undo Removal in AppCommands) await MainActor.run { if appState.currentPage == .plugins { NotificationCenter.default.post(name: NSNotification.Name("PluginsViewShouldRefresh"), object: nil) } else if appState.currentPage == .fileSearch { NotificationCenter.default.post(name: NSNotification.Name("FileSearchViewShouldUndo"), object: nil) } else if appState.currentPage == .orphans { NotificationCenter.default.post(name: NSNotification.Name("ZombieViewShouldRefresh"), object: nil) } else if appState.currentPage == .packages { NotificationCenter.default.post(name: NSNotification.Name("PackagesViewShouldRefresh"), object: nil) } else if appState.currentPage == .development { NotificationCenter.default.post(name: NSNotification.Name("DevelopmentViewShouldRefresh"), object: nil) } else { // Use default non-streaming mode for undo (needs full AppInfo) loadApps(folderPaths: fsm.folderPaths) // After reload, if we're viewing files, refresh the file view if appState.currentView == .files { Task { @MainActor in try? await Task.sleep(nanoseconds: 500_000_000) showAppInFiles(appInfo: appState.appInfo, appState: appState, locations: locations) } } } } // Close sheet if no history remains if historyManager.history.isEmpty { await MainActor.run { appState.showDeleteHistory = false } } } catch { printOS("❌ Failed to restore records: \(error.localizedDescription)") isRestoring = false } } } } // MARK: - History Record Row struct HistoryRecordRow: View { let record: UndoHistoryRecord let isSelected: Bool let isValid: Bool let onToggle: () -> Void @Environment(\.colorScheme) var colorScheme var body: some View { HStack(spacing: 12) { // Checkbox button Button { onToggle() } label: { EmptyView() } .buttonStyle(CircleCheckboxButtonStyle(isSelected: isSelected)) .disabled(!isValid) VStack(alignment: .leading, spacing: 4) { // App name and timestamp HStack { Text(record.appName) .font(.headline) .foregroundStyle(isValid ? ThemeColors.shared(for: colorScheme).primaryText : ThemeColors.shared(for: colorScheme).secondaryText.opacity(0.5)) Spacer() Text(formatRelativeTime(record.timestamp)) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } // File count HStack { Text("\(record.fileCount) file\(record.fileCount == 1 ? "" : "s")") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) if !isValid { Text(verbatim: "•") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text("Files no longer in trash") .font(.caption) .foregroundStyle(.orange) } } } Spacer() } .padding(.horizontal, 12) .padding(.vertical, 10) .background(isSelected ? ThemeColors.shared(for: colorScheme).accent.opacity(0.1) : Color.clear) } } ================================================ FILE: Pearcleaner/Views/DevelopmentView.swift ================================================ // // DevelopmentView.swift // Pearcleaner // // Created by Alin Lupascu on 11/15/24. // import SwiftUI import AlinFoundation struct PathEnv: Identifiable, Hashable, Equatable { let id = UUID() let name: String let paths: [String] } struct EnvironmentCleanerView: View { @EnvironmentObject var appState: AppState @Environment(\.colorScheme) var colorScheme @ObservedObject private var consoleManager = GlobalConsoleManager.shared @State private var paths: [PathEnv] = [] @State private var selectedPaths: Set = [] @State private var searchText: String = "" @State private var lastRefreshDate: Date? @State private var isLoading: Bool = false @State private var collapsedCategories: Set = [] @AppStorage("settings.interface.scrollIndicators") private var scrollIndicators: Bool = false @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true // Store all paths, including "All" and each environment private var allPaths: [PathEnv] { let realPaths = PathLibrary.getPaths() let combined = realPaths.flatMap { $0.paths } return [PathEnv(name: "All", paths: combined)] + realPaths } private func refreshPaths() { GlobalConsoleManager.shared.appendOutput("Refreshing development paths...\n", source: CurrentPage.development.title) isLoading = true Task { await refreshPathsAsync() } } private func refreshPathsAsync() async { let fileManager = FileManager.default let refreshedPaths = allPaths.map { env in let validPaths = env.paths.filter { let expanded = NSString(string: $0).expandingTildeInPath var isDir: ObjCBool = false if fileManager.fileExists(atPath: expanded, isDirectory: &isDir) { if isDir.boolValue { if let contents = try? fileManager.contentsOfDirectory(atPath: expanded) { return contents.filter { $0 != ".DS_Store" }.isEmpty == false } } else { return true } } return false } return PathEnv(name: env.name, paths: validPaths) } await MainActor.run { self.paths = refreshedPaths self.lastRefreshDate = Date() self.isLoading = false GlobalConsoleManager.shared.appendOutput("✓ Refreshed development paths\n", source: CurrentPage.development.title) // Update selected environment to its refreshed version if let selected = appState.selectedEnvironment { if let updated = paths.first(where: { $0.name == selected.name }) { appState.selectedEnvironment = updated } else { appState.selectedEnvironment = nil } } else { // Default to "All" if no environment is selected and paths exist if let allEnvironment = paths.first(where: { $0.name == "All" }), !allEnvironment.paths.isEmpty { appState.selectedEnvironment = allEnvironment } } // Clear selection when refreshing paths selectedPaths.removeAll() } } // Computed property for filtered paths private var filteredPaths: [PathEnv] { guard let selectedEnvironment = appState.selectedEnvironment else { return [] } if searchText.isEmpty { return [selectedEnvironment] } let filteredEnvironment = PathEnv( name: selectedEnvironment.name, paths: selectedEnvironment.paths.filter { path in path.localizedCaseInsensitiveContains(searchText) } ) return [filteredEnvironment] } // Total count of paths for stats private var totalPathsCount: Int { if let selectedEnvironment = appState.selectedEnvironment { return selectedEnvironment.paths.count } return 0 } var body: some View { VStack(alignment: .leading, spacing: 0) { // Search bar HStack { Image(systemName: "magnifyingglass") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) TextField("Search...", text: $searchText) .textFieldStyle(.plain) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) if !searchText.isEmpty { Button { searchText = "" } label: { Image(systemName: "xmark.circle.fill") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .buttonStyle(.plain) } } .padding(.horizontal, 12) .padding(.vertical, 8) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) .controlGroup(Capsule(style: .continuous), level: .primary) .padding(.top, 5) if let selectedEnvironment = appState.selectedEnvironment, filteredPaths.count > 0 { // Stats header HStack { let filteredCount = filteredPaths.first?.paths.count ?? 0 Text("\(filteredCount) path\(filteredCount == 1 ? "" : "s")") .font(.caption) .monospacedDigit() .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) if isLoading { Text("Loading...") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } Spacer() if let lastRefresh = lastRefreshDate { TimelineView(.periodic(from: lastRefresh, by: 1.0)) { _ in Text("Updated \(formatRelativeTime(lastRefresh))") .font(.caption) .monospacedDigit() .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } } .padding(.vertical) // Add workspace storage cleaner for certain IDEs let workspaceIDEs = ["VS Code", "Cursor", "Zed"] let packageManagers = ["Pip"] if workspaceIDEs.contains(selectedEnvironment.name) { WorkspaceStorageCleanerView(ideName: selectedEnvironment.name) .id(selectedEnvironment.name) .padding(.bottom, 10) } else if packageManagers.contains(selectedEnvironment.name) { PipPackageCleanerView() .id(selectedEnvironment.name) .padding(.bottom, 10) } ScrollView { LazyVStack(spacing: 10) { if selectedEnvironment.name == "All" { // Show categorized view for "All" environment ForEach(paths.filter { !$0.paths.isEmpty && $0.name != "All" }, id: \.name) { environment in let environmentPaths = environment.paths.filter { path in searchText.isEmpty || path.localizedCaseInsensitiveContains(searchText) } if !environmentPaths.isEmpty { let isCollapsed = environmentPaths.isEmpty || collapsedCategories.contains(environment.name) GroupBox { if !isCollapsed { ForEach(environmentPaths, id: \.self) { path in PathRowView( path: path, isSelected: Binding( get: { selectedPaths.contains(path) }, set: { isSelected in if isSelected { selectedPaths.insert(path) } else { selectedPaths.remove(path) } } ) ) { refreshPaths() selectedPaths.remove(path) } } .transition(.opacity.combined(with: .slide)) } } .groupBoxStyle(.collapsible( title: environment.name, count: environmentPaths.count, isCollapsed: isCollapsed, onToggle: { if isCollapsed { collapsedCategories.remove(environment.name) } else { collapsedCategories.insert(environment.name) } } )) } } } else { // Show regular path list for specific environments if let filteredEnvironment = filteredPaths.first { ForEach(filteredEnvironment.paths, id: \.self) { path in PathRowView( path: path, isSelected: Binding( get: { selectedPaths.contains(path) }, set: { isSelected in if isSelected { selectedPaths.insert(path) } else { selectedPaths.remove(path) } } ) ) { refreshPaths() selectedPaths.remove(path) if let env = appState.selectedEnvironment, paths.first(where: { $0.name == env.name })?.paths.isEmpty ?? true { appState.selectedEnvironment = nil } } } } } } } .scrollIndicators(scrollIndicators ? .automatic : .never) } else { VStack(alignment: .center) { Spacer() Text("Select an environment to view stored cache") .font(.title2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() } .frame(maxWidth: .infinity) } } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(.horizontal, 20) .safeAreaInset(edge: .bottom) { if !selectedPaths.isEmpty { // Show selection toolbar only when items are selected if let selectedEnvironment = appState.selectedEnvironment, !selectedEnvironment.paths.isEmpty { HStack { Spacer() HStack(spacing: 10) { let availablePaths = selectedEnvironment.name == "All" ? paths.filter { !$0.paths.isEmpty && $0.name != "All" }.flatMap { env in env.paths.filter { path in searchText.isEmpty || path.localizedCaseInsensitiveContains(searchText) } } : (filteredPaths.first?.paths ?? []) Button(selectedPaths.count == availablePaths.count ? "Deselect All" : "Select All") { if selectedPaths.count == availablePaths.count { selectedPaths.removeAll() } else { selectedPaths = Set(availablePaths) } } .buttonStyle(ControlGroupButtonStyle( foregroundColor: ThemeColors.shared(for: colorScheme).accent, shape: Capsule(style: .continuous), level: .primary, skipControlGroup: true )) Divider().frame(height: 10) Button("Delete \(selectedPaths.count) Selected Folders") { showCustomAlert(title: "Warning", message: "This will delete \(selectedPaths.count) selected folders. Are you sure?", style: .warning, onOk: { GlobalConsoleManager.shared.appendOutput("Starting deletion of \(selectedPaths.count) development folder(s)...\n", source: CurrentPage.development.title) let urls = selectedPaths.map { URL(fileURLWithPath: NSString(string: $0).expandingTildeInPath) } let bundleName = "Development - Folders (\(selectedPaths.count))" let _ = FileManagerUndo.shared.deleteFiles(at: urls, bundleName: bundleName) GlobalConsoleManager.shared.appendOutput("✓ Completed deletion of \(selectedPaths.count) folder(s)\n", source: CurrentPage.development.title) selectedPaths.removeAll() refreshPaths() if let env = appState.selectedEnvironment, paths.first(where: { $0.name == env.name })?.paths.isEmpty ?? true { appState.selectedEnvironment = nil } }) } .buttonStyle(ControlGroupButtonStyle( foregroundColor: ThemeColors.shared(for: colorScheme).accent, shape: Capsule(style: .continuous), level: .primary, skipControlGroup: true )) Divider().frame(height: 10) Button("Delete \(selectedPaths.count) Selected Contents") { showCustomAlert(title: "Warning", message: "This will delete the contents of \(selectedPaths.count) selected folders. Are you sure?", style: .warning, onOk: { GlobalConsoleManager.shared.appendOutput("Starting deletion of contents from \(selectedPaths.count) folder(s)...\n", source: CurrentPage.development.title) var allContentURLs: [URL] = [] let fm = FileManager.default for path in selectedPaths { let expanded = NSString(string: path).expandingTildeInPath if let contents = try? fm.contentsOfDirectory(atPath: expanded) { for item in contents { let itemPath = (expanded as NSString).appendingPathComponent(item) allContentURLs.append(URL(fileURLWithPath: itemPath)) } } } if !allContentURLs.isEmpty { let bundleName = "Development - Contents (\(selectedPaths.count))" let _ = FileManagerUndo.shared.deleteFiles(at: allContentURLs, bundleName: bundleName) GlobalConsoleManager.shared.appendOutput("✓ Completed deletion of contents\n", source: CurrentPage.development.title) } selectedPaths.removeAll() refreshPaths() if let env = appState.selectedEnvironment, paths.first(where: { $0.name == env.name })?.paths.isEmpty ?? true { appState.selectedEnvironment = nil } }) } .buttonStyle(ControlGroupButtonStyle( foregroundColor: ThemeColors.shared(for: colorScheme).accent, shape: Capsule(style: .continuous), level: .primary, skipControlGroup: true )) } .controlGroup(Capsule(style: .continuous), level: .primary) Spacer() } .padding([.horizontal, .bottom]) } } } .onAppear { refreshPaths() } .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("DevelopmentViewShouldRefresh"))) { _ in refreshPaths() } .toolbarBackground(.hidden, for: .windowToolbar) .toolbar { TahoeToolbarItem(placement: .navigation) { VStack(alignment: .leading){ Text("Development Environments").foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText).font(.title2).fontWeight(.bold) Text("Clean stored files and cache for common IDEs") .font(.callout).foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } ToolbarItem { Spacer() } TahoeToolbarItem(isGroup: true) { Button { withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) { consoleManager.showConsole.toggle() } } label: { Label("Console", systemImage: consoleManager.showConsole ? "terminal.fill" : "terminal") } .help("Toggle console output") Menu { ForEach(paths, id: \.self) { environment in Group { if environment.paths.isEmpty { Text(verbatim: "\(environment.name) (0)") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } else { Button { appState.selectedEnvironment = environment selectedPaths.removeAll() } label: { Text(verbatim: "\(environment.name) (\(environment.paths.count))") .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } } } } } label: { Label(appState.selectedEnvironment?.name ?? "Select Environment", systemImage: "list.bullet") } .labelStyle(.titleAndIcon) .menuIndicator(.hidden) Button { refreshPaths() } label: { Label("Refresh", systemImage: "arrow.counterclockwise") } } } } } struct PathRowView: View { @Environment(\.colorScheme) var colorScheme let path: String @Binding var isSelected: Bool let onDelete: () -> Void @State private var exists: Bool = false @State private var isEmpty: Bool = false @State private var matchingPaths: [String] = [] @State private var sizeLoading: Bool = true @State private var size: Int64 = 0 var body: some View { VStack(alignment: .leading, spacing: 10) { if !matchingPaths.isEmpty { ForEach(matchingPaths, id: \.self) { matchedPath in HStack(spacing: 15) { // Selection checkbox - OUTSIDE the background Button(action: { isSelected.toggle() }) { Image(systemName: isSelected ? "checkmark.circle.fill" : "circle") .foregroundStyle(isSelected ? .blue : ThemeColors.shared(for: colorScheme).secondaryText) .font(.title3) } .buttonStyle(.plain) // Content with background VStack(alignment: .leading, spacing: 10) { HStack { Button { openInFinder(matchedPath) } label: { Image(systemName: "folder") } .buttonStyle(.plain) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) matchedPath.pathWithArrows() .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .font(.headline) Spacer() Text(formatByte(size: size).human) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) HStack(spacing: 10) { Button("Delete Folder") { deleteFolder(matchedPath) } .buttonStyle(.borderless) .controlSize(.mini) .foregroundStyle(.red) .help("Delete the folder") Divider().frame(height: 10) Button("Delete Contents") { deleteFolderContents(matchedPath) } .buttonStyle(.borderless) .controlSize(.mini) .foregroundStyle(.red) .disabled(isEmpty) .help("Delete all files within this folder") } } .onAppear { DispatchQueue.global(qos: .userInitiated).async { if let url = URL(string: matchedPath) { let calculatedSize = totalSizeOnDisk(for: url) DispatchQueue.main.async { self.size = calculatedSize } } } } } .frame(maxWidth: .infinity) .padding() .background(ThemeColors.shared(for: colorScheme).secondaryBG.clipShape(RoundedRectangle(cornerRadius: 8))) } } } else { HStack { Text(expandTilde(path)) .lineLimit(1) .truncationMode(.middle) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() Text("Not Found") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .frame(maxWidth: .infinity) .padding() .background(ThemeColors.shared(for: colorScheme).secondaryBG.clipShape(RoundedRectangle(cornerRadius: 8))) } } .onAppear { checkPath(path) } } private func checkPath(_ path: String) { let expandedPath = NSString(string: path).expandingTildeInPath let fileManager = FileManager.default if path.contains("*") { // Handle wildcard paths if expandedPath.contains("/*/") { // Handle middle wildcard like ~/.gem/ruby/*/cache/ let components = expandedPath.split(separator: "*", maxSplits: 1, omittingEmptySubsequences: true) guard components.count == 2 else { exists = false matchingPaths = [] return } let basePath = String(components[0]) // Path before wildcard let remainderPath = String(components[1]) // Path after wildcard do { let contents = try fileManager.contentsOfDirectory(atPath: basePath) .filter { $0 != ".DS_Store" } // Exclude .DS_Store let matchingFolders = contents.filter { fileManager.fileExists(atPath: (basePath as NSString).appendingPathComponent($0), isDirectory: nil) }.map { (basePath as NSString).appendingPathComponent($0) } matchingPaths = matchingFolders.compactMap { folder in let fullPath = (folder as NSString).appendingPathComponent(remainderPath) return fileManager.fileExists(atPath: fullPath) ? fullPath : nil } exists = !matchingPaths.isEmpty isEmpty = matchingPaths.allSatisfy { folder in if let innerContents = try? fileManager.contentsOfDirectory(atPath: folder) { return innerContents.filter { $0 != ".DS_Store" }.isEmpty } return true } } catch { exists = false matchingPaths = [] } } else { // Handle partial folder wildcard like ~/Library/Application Support/Google/AndroidStudio*/ let basePath = NSString(string: expandedPath).deletingLastPathComponent let partialComponent = NSString(string: expandedPath).lastPathComponent.replacingOccurrences(of: "*", with: "") do { let contents = try fileManager.contentsOfDirectory(atPath: basePath) .filter { $0 != ".DS_Store" } // Exclude .DS_Store matchingPaths = contents.filter { $0.hasPrefix(partialComponent) } .map { (basePath as NSString).appendingPathComponent($0) } exists = !matchingPaths.isEmpty isEmpty = matchingPaths.allSatisfy { folder in if let innerContents = try? fileManager.contentsOfDirectory(atPath: folder) { return innerContents.filter { $0 != ".DS_Store" }.isEmpty } return true } } catch { exists = false matchingPaths = [] } } } else { // Normal path handling exists = fileManager.fileExists(atPath: expandedPath) if exists { if let contents = try? fileManager.contentsOfDirectory(atPath: expandedPath) { isEmpty = contents.filter { $0 != ".DS_Store" }.isEmpty // Exclude .DS_Store } else { isEmpty = true } matchingPaths = [expandedPath] } else { matchingPaths = [] } } } private func expandTilde(_ path: String) -> String { return NSString(string: path).expandingTildeInPath } private func openInFinder(_ matchedPath: String) { let fileManager = FileManager.default if fileManager.fileExists(atPath: matchedPath) { NSWorkspace.shared.open(URL(fileURLWithPath: matchedPath)) } } private func deleteFolderContents(_ matchedPath: String) { let fileManager = FileManager.default do { let contents = try fileManager.contentsOfDirectory(atPath: matchedPath) var contentURLs: [URL] = [] for item in contents { let itemPath = (matchedPath as NSString).appendingPathComponent(item) contentURLs.append(URL(fileURLWithPath: itemPath)) } if !contentURLs.isEmpty { let folderName = (matchedPath as NSString).lastPathComponent let bundleName = "Development - \(folderName) Contents" let _ = FileManagerUndo.shared.deleteFiles(at: contentURLs, bundleName: bundleName) } checkPath(path) // Recheck the state after deletion } catch { printOS("Error deleting contents of folder: \(error)") } } private func deleteFolder(_ matchedPath: String) { let folderName = (matchedPath as NSString).lastPathComponent let bundleName = "Development - \(folderName)" let url = URL(fileURLWithPath: matchedPath) let result = FileManagerUndo.shared.deleteFiles(at: [url], bundleName: bundleName) if result { onDelete() } } } struct WorkspaceStorageCleanerView: View { @AppStorage("settings.interface.scrollIndicators") private var scrollIndicators: Bool = false @Environment(\.colorScheme) var colorScheme let ideName: String @State private var orphanedWorkspaces: [OrphanedWorkspace] = [] @State private var isScanning = false @State private var lastScanDate: Date? struct OrphanedWorkspace { let id = UUID() let name: String let path: String let folderPath: String let size: String } var body: some View { VStack(alignment: .leading, spacing: 12) { HStack { Image(systemName: "macwindow") .resizable() .scaledToFit() .frame(width: 30, height: 30) VStack(alignment: .leading, spacing: 4) { Text("Workspace Storage Cleaner") .font(.headline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Text("Remove workspace storage for deleted project folders") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } Spacer() Button(action: scanForOrphanedWorkspaces) { HStack(spacing: 6) { if isScanning { ProgressView() .scaleEffect(0.8) } else { Image(systemName: "magnifyingglass") } Text(isScanning ? "Scanning..." : "Scan") } } .disabled(isScanning) .controlSize(.small) .buttonStyle(.plain) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) .padding(.vertical, 8) .padding(.horizontal, 14) .controlGroup(Capsule(style: .continuous), level: .primary) } if !orphanedWorkspaces.isEmpty { Divider() VStack(alignment: .leading, spacing: 8) { HStack { Text("Found \(orphanedWorkspaces.count) orphaned workspace\(orphanedWorkspaces.count == 1 ? "" : "s")") .font(.subheadline) .foregroundStyle(.orange) Spacer() } ScrollView { ForEach(orphanedWorkspaces, id: \.id) { workspace in HStack { VStack(alignment: .leading, spacing: 2) { Text(workspace.name) .font(.caption) .lineLimit(1) .truncationMode(.middle) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Text(verbatim: "\(workspace.folderPath)") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .lineLimit(1) .truncationMode(.middle) } Spacer() Text(workspace.size) .font(.caption2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Button("Delete") { cleanOrphanedWorkspace(workspace) } .buttonStyle(.borderless) .controlSize(.mini) .foregroundStyle(.red) } .padding(.horizontal, 8) .padding(.vertical, 4) .background(ThemeColors.shared(for: colorScheme).secondaryText.opacity(0.1)) .cornerRadius(6) } } .scrollIndicators(scrollIndicators ? .automatic : .never) .frame(maxHeight: 180) .fixedSize(horizontal: false, vertical: true) HStack { Spacer() Button("Delete All") { cleanAllOrphanedWorkspaces() } .disabled(isScanning) .controlSize(.small) .buttonStyle(.plain) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) .padding(.vertical, 8) .padding(.horizontal, 14) .controlGroup(Capsule(style: .continuous), level: .primary) Button("Cancel") { cancelWorkspaceCleanup() } .disabled(isScanning) .controlSize(.small) .buttonStyle(.plain) .padding(.vertical, 8) .padding(.horizontal, 14) .controlGroup(Capsule(style: .continuous), level: .primary) } } } else if lastScanDate != nil { Text("No orphaned workspaces found") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .italic() } } .padding() .background(ThemeColors.shared(for: colorScheme).primaryText.opacity(0.05)) .cornerRadius(10) } private func scanForOrphanedWorkspaces() { isScanning = true orphanedWorkspaces = [] Task { let found = await findOrphanedWorkspaces() await MainActor.run { self.orphanedWorkspaces = found self.lastScanDate = Date() self.isScanning = false } } } private func findOrphanedWorkspaces() async -> [OrphanedWorkspace] { let configPath = ideName == "VS Code" ? "~/Library/Application Support/Code" : "~/Library/Application Support/Cursor" let expandedPath = NSString(string: configPath).expandingTildeInPath let workspaceStoragePath = "\(expandedPath)/User/workspaceStorage" let fileManager = FileManager.default var orphaned: [OrphanedWorkspace] = [] guard let workspaceDirs = try? fileManager.contentsOfDirectory(atPath: workspaceStoragePath) else { return orphaned } for workspaceDir in workspaceDirs { let workspacePath = "\(workspaceStoragePath)/\(workspaceDir)" let workspaceJsonPath = "\(workspacePath)/workspace.json" if fileManager.fileExists(atPath: workspaceJsonPath) { if let folderPath = extractFolderPath(from: workspaceJsonPath), !fileManager.fileExists(atPath: folderPath) { let size = calculateDirectorySize(at: workspacePath) orphaned.append(OrphanedWorkspace( name: workspaceDir, path: workspacePath, folderPath: folderPath, size: size )) } } } return orphaned } private func extractFolderPath(from workspaceJsonPath: String) -> String? { guard let data = try? Data(contentsOf: URL(fileURLWithPath: workspaceJsonPath)), let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], let folder = json["folder"] as? String else { return nil } // Remove file:// prefix and decode URL encoding var cleanPath = folder.replacingOccurrences(of: "file://", with: "") cleanPath = cleanPath.removingPercentEncoding ?? cleanPath cleanPath = cleanPath.replacingOccurrences(of: "+", with: " ") return cleanPath } private func calculateDirectorySize(at path: String) -> String { let url = URL(fileURLWithPath: path) guard let enumerator = FileManager.default.enumerator( at: url, includingPropertiesForKeys: [.fileSizeKey, .isDirectoryKey], options: [.skipsHiddenFiles] ) else { return "0 B" } var totalSize: Int64 = 0 for case let fileURL as URL in enumerator { guard let resourceValues = try? fileURL.resourceValues(forKeys: [.fileSizeKey, .isDirectoryKey]), let fileSize = resourceValues.fileSize, let isDirectory = resourceValues.isDirectory, !isDirectory else { continue } totalSize += Int64(fileSize) } return ByteCountFormatter.string(fromByteCount: totalSize, countStyle: .file) } private func cleanOrphanedWorkspace(_ workspace: OrphanedWorkspace) { let bundleName = "Development - Workspace (\(workspace.name))" let url = URL(fileURLWithPath: workspace.path) let result = FileManagerUndo.shared.deleteFiles(at: [url], bundleName: bundleName) if result { orphanedWorkspaces.removeAll { $0.id == workspace.id } } } private func cleanAllOrphanedWorkspaces() { let urls = orphanedWorkspaces.map { URL(fileURLWithPath: $0.path) } let bundleName = "Development - Workspaces (\(orphanedWorkspaces.count))" let result = FileManagerUndo.shared.deleteFiles(at: urls, bundleName: bundleName) if result { orphanedWorkspaces.removeAll() } } private func cancelWorkspaceCleanup() { orphanedWorkspaces = [] lastScanDate = nil isScanning = false } } // MARK: - Pip Package Cleaner View struct PipPackageCleanerView: View { @Environment(\.colorScheme) var colorScheme @State private var packages: [PipPackage] = [] @State private var isScanning = false @State private var selectedPackages = Set() @State private var lastScanDate: Date? = nil @State private var showAlert = false @State private var alertMessage = "" @State private var pythonPath: String = "/usr/bin/python3" @State private var sitePackages: [String] = [] @State private var showFileImporter: Bool = false @State private var sortOption: PipSortOption = .name struct PipPackage: Identifiable { let id = UUID() let name: String let version: String var size: String? var sizeBytes: Int64? } enum PipSortOption: String, CaseIterable { case name = "Name" case size = "Size" var systemImage: String { switch self { case .name: return "list.bullet" case .size: return "arrow.up.arrow.down" } } } var body: some View { VStack(alignment: .leading, spacing: 12) { HStack { Image(systemName: "shippingbox") .resizable() .scaledToFit() .frame(width: 30, height: 30) VStack(alignment: .leading, spacing: 4) { Text("Pip Packages") .font(.headline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) HStack(spacing: 6) { Text("Python:") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text(pythonPath) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .lineLimit(1) .truncationMode(.middle) Button { showFileImporter = true } label: { Text("Change") .font(.caption) } .buttonStyle(.plain) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) .controlSize(.small) .padding(.vertical, 4) .padding(.horizontal, 8) .controlGroup(Capsule(style: .continuous), level: .secondary) } } Spacer() Button(action: { Task { await scanForInstalledPackages() } }) { HStack(spacing: 6) { if isScanning { ProgressView() .scaleEffect(0.8) } else { Image(systemName: "arrow.clockwise") } Text(isScanning ? "Scanning..." : "Scan Packages") } } .disabled(isScanning) .controlSize(.small) .buttonStyle(.plain) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) .padding(.vertical, 8) .padding(.horizontal, 14) .controlGroup(Capsule(style: .continuous), level: .primary) } if !packages.isEmpty { VStack(alignment: .leading, spacing: 8) { HStack { Text("\(packages.count) package\(packages.count == 1 ? "" : "s") installed") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() Menu { ForEach(PipSortOption.allCases, id: \.self) { option in Button { sortOption = option } label: { Label(option.rawValue, systemImage: option.systemImage) } } } label: { Label(sortOption.rawValue, systemImage: sortOption.systemImage) .font(.caption) } .labelStyle(.titleAndIcon) .buttonStyle(.plain) .help("Sort by \(sortOption.rawValue)") if !selectedPackages.isEmpty { Button { Task { await uninstallSelectedPackages() } } label: { Label("Uninstall Selected (\(selectedPackages.count))", systemImage: "trash") .font(.caption) } .buttonStyle(ControlGroupButtonStyle( foregroundColor: .red, shape: Capsule(style: .continuous), level: .secondary, skipControlGroup: true )) } } ScrollView { VStack(spacing: 4) { ForEach(sortedPackages()) { package in HStack(spacing: 12) { Button { if selectedPackages.contains(package.id) { selectedPackages.remove(package.id) } else { selectedPackages.insert(package.id) } } label: { Image(systemName: selectedPackages.contains(package.id) ? "checkmark.square.fill" : "square") .foregroundStyle(selectedPackages.contains(package.id) ? ThemeColors.shared(for: colorScheme).accent : ThemeColors.shared(for: colorScheme).secondaryText) } .buttonStyle(.plain) HStack(spacing: 6) { Text(verbatim: package.name) .font(.system(.body, design: .monospaced)) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Text(verbatim: "v\(package.version)") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } Spacer() if let size = package.size { Text(size) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } else { Text("Calculating...") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } Button { Task { await uninstallPackage(package) } } label: { Image(systemName: "trash") .foregroundStyle(.red) } .buttonStyle(.plain) .help("Uninstall \(package.name)") } .padding(.horizontal, 8) .padding(.vertical, 6) .background( RoundedRectangle(cornerRadius: 6) .fill(Color.primary.opacity(0.05)) ) .task(id: package.id) { // Calculate size lazily when row appears if package.size == nil { let packageId = package.id // Capture ID before async let packageName = package.name // Capture name before async let (sizeBytes, sizeFormatted) = await calculatePackageSize(packageName: packageName) await MainActor.run { if let index = packages.firstIndex(where: { $0.id == packageId }) { packages[index].size = sizeFormatted packages[index].sizeBytes = sizeBytes } } } } } } } .frame(maxHeight: .infinity) HStack { Spacer() Button("Uninstall All") { Task { await uninstallAllPackages() } } .disabled(isScanning || packages.isEmpty) .controlSize(.small) .buttonStyle(.plain) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) .padding(.vertical, 8) .padding(.horizontal, 14) .controlGroup(Capsule(style: .continuous), level: .primary) Button("Cancel") { cancelPackageScan() } .disabled(isScanning) .controlSize(.small) .buttonStyle(.plain) .padding(.vertical, 8) .padding(.horizontal, 14) .controlGroup(Capsule(style: .continuous), level: .primary) } } .frame(minHeight: 250, maxHeight: 300) } else if lastScanDate != nil { Text("No packages found") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .italic() } } .padding() .background(ThemeColors.shared(for: colorScheme).primaryText.opacity(0.05)) .cornerRadius(10) .alert("Error", isPresented: $showAlert) { Button("Okay", role: .cancel) { } } message: { Text(alertMessage) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } .fileImporter( isPresented: $showFileImporter, allowedContentTypes: [.executable, .unixExecutable], allowsMultipleSelection: false ) { result in switch result { case .success(let urls): if let url = urls.first { pythonPath = url.path } case .failure(let error): alertMessage = "Failed to select Python: \(error.localizedDescription)" showAlert = true } } .task { // Detect active Python and site-packages on view appear let detectedPath = await detectActivePython() pythonPath = detectedPath let detectedPackages = await detectSitePackages(pythonPath: pythonPath) sitePackages = detectedPackages } .onChange(of: pythonPath) { newPath in // When Python path changes, detect new site-packages Task { let detectedPackages = await detectSitePackages(pythonPath: newPath) sitePackages = detectedPackages } } .onChange(of: sortOption) { _ in // When sort option changes, calculate sizes for packages that haven't been calculated yet if sortOption == .size { Task { // Trigger size calculation for all packages without sizes for package in packages where package.size == nil { let (sizeBytes, sizeFormatted) = await calculatePackageSize(packageName: package.name) await MainActor.run { if let index = packages.firstIndex(where: { $0.id == package.id }) { packages[index].size = sizeFormatted packages[index].sizeBytes = sizeBytes } } } // Sort after all sizes are calculated await MainActor.run { packages.sort { ($0.sizeBytes ?? 0) > ($1.sizeBytes ?? 0) } } } } else { // Sort by name immediately packages.sort { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } } } } // MARK: - Scan for Installed Packages private func scanForInstalledPackages() async { isScanning = true selectedPackages.removeAll() let packagesFound = await findInstalledPackages() await MainActor.run { // Sort packages based on selected sort option switch sortOption { case .name: self.packages = packagesFound.sorted { $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending } case .size: self.packages = packagesFound.sorted { ($0.sizeBytes ?? 0) > ($1.sizeBytes ?? 0) } // Largest first } self.lastScanDate = Date() } isScanning = false } private func sortedPackages() -> [PipPackage] { switch sortOption { case .name: return packages.sorted { $0.name.lowercased() < $1.name.lowercased() } case .size: return packages.sorted { ($0.sizeBytes ?? 0) > ($1.sizeBytes ?? 0) } } } private func findInstalledPackages() async -> [PipPackage] { var foundPackages: [PipPackage] = [] do { // Run pip list --format=json let listOutput = try await runPipCommand(["list", "--format=json"]) // Parse JSON guard let data = listOutput.data(using: .utf8) else { return [] } struct PipListItem: Codable { let name: String let version: String } let decoder = JSONDecoder() let items = try decoder.decode([PipListItem].self, from: data) // Create packages immediately without size (will be calculated lazily on row appear) for item in items { foundPackages.append(PipPackage( name: item.name, version: item.version, size: nil, sizeBytes: nil )) } } catch { // Silent error handling - return empty array } return foundPackages } private func calculatePackageSize(packageName: String) async -> (Int64, String) { do { // Use pip show (without -f) to get package location let showOutput = try await runPipCommand(["show", packageName]) // Check if package not found if showOutput.contains("WARNING: Package(s) not found") { return (0, "0 KB") } // Parse Location line var location = "" for line in showOutput.components(separatedBy: "\n") { let trimmed = line.trimmingCharacters(in: .whitespaces) if trimmed.starts(with: "Location:") { location = trimmed.replacingOccurrences(of: "Location:", with: "") .trimmingCharacters(in: .whitespaces) break } } guard !location.isEmpty else { return (0, "0 KB") } // Find package directories using FileManager let fileManager = FileManager.default guard fileManager.fileExists(atPath: location) else { return (0, "0 KB") } let normalizedName = packageName.replacingOccurrences(of: "-", with: "_").lowercased() var packagePaths: [String] = [] do { let contents = try fileManager.contentsOfDirectory(atPath: location) for item in contents { let itemLower = item.lowercased() // Match package directory and dist-info directory if itemLower == normalizedName || itemLower.starts(with: normalizedName + "-") || itemLower.starts(with: packageName.lowercased() + "-") { let itemPath = (location as NSString).appendingPathComponent(item) packagePaths.append(itemPath) } } } catch { return (0, "0 KB") } guard !packagePaths.isEmpty else { return (0, "0 KB") } // Calculate total size using totalSizeOnDisk function var totalSizeBytes: Int64 = 0 for path in packagePaths { let url = URL(fileURLWithPath: path) totalSizeBytes += totalSizeOnDisk(for: url) } let formatted = ByteCountFormatter.string(fromByteCount: totalSizeBytes, countStyle: .file) return (totalSizeBytes, formatted) } catch { return (0, "0 KB") } } // MARK: - Uninstall Methods private func uninstallPackage(_ package: PipPackage) async { do { _ = try await runPipCommand(["uninstall", "-y", package.name]) await MainActor.run { packages.removeAll { $0.id == package.id } selectedPackages.remove(package.id) } } catch { await MainActor.run { alertMessage = "Failed to uninstall \(package.name): \(error.localizedDescription)" showAlert = true } } } private func uninstallSelectedPackages() async { let packagesToUninstall = packages.filter { selectedPackages.contains($0.id) } for package in packagesToUninstall { await uninstallPackage(package) } } private func uninstallAllPackages() async { for package in packages { await uninstallPackage(package) } } private func cancelPackageScan() { packages = [] selectedPackages = [] lastScanDate = nil isScanning = false } // MARK: - Helper Methods private func runPipCommand(_ arguments: [String], attempt: Int = 1) async throws -> String { return try await withCheckedThrowingContinuation { continuation in DispatchQueue.global(qos: .userInitiated).async { [pythonPath] in let process = Process() process.executableURL = URL(fileURLWithPath: pythonPath) // Add --disable-pip-version-check flag for faster execution var pipArgs = ["-m", "pip"] pipArgs.append(contentsOf: arguments) if !arguments.contains("--disable-pip-version-check") { pipArgs.append("--disable-pip-version-check") } process.arguments = pipArgs // Set environment, prioritizing the selected Python's directory var env = ProcessInfo.processInfo.userEnvironment // Extract the directory containing the Python executable let pythonDir = (pythonPath as NSString).deletingLastPathComponent // Prepend Python's directory to PATH to ensure we use the correct pip let oldPath = env["PATH"] ?? "" if !oldPath.isEmpty { env["PATH"] = "\(pythonDir):\(oldPath)" } else { env["PATH"] = pythonDir } process.environment = env // Close stdin to prevent any waiting process.standardInput = nil let pipe = Pipe() let errorPipe = Pipe() process.standardOutput = pipe process.standardError = errorPipe do { try process.run() // Read output asynchronously to prevent pipe buffer deadlock // (packages with large file lists like botocore, numpy, pip can exceed pipe buffer) var outputData = Data() var errorData = Data() let outputHandle = pipe.fileHandleForReading let errorHandle = errorPipe.fileHandleForReading // Set up non-blocking read handlers outputHandle.readabilityHandler = { handle in outputData.append(handle.availableData) } errorHandle.readabilityHandler = { handle in errorData.append(handle.availableData) } // Implement timeout (5 seconds) to prevent infinite hanging let timeoutTask = DispatchWorkItem { if process.isRunning { process.terminate() } } DispatchQueue.global().asyncAfter(deadline: .now() + 5.0, execute: timeoutTask) process.waitUntilExit() timeoutTask.cancel() // Cancel timeout if process finished normally // Clean up handlers and read any remaining data outputHandle.readabilityHandler = nil errorHandle.readabilityHandler = nil outputData.append(outputHandle.readDataToEndOfFile()) errorData.append(errorHandle.readDataToEndOfFile()) if process.terminationStatus == 0 { let output = String(data: outputData, encoding: .utf8) ?? "" continuation.resume(returning: output) } else if process.terminationStatus == 15 && attempt < 2 { // SIGTERM from timeout // Retry once if timed out Task { do { let result = try await self.runPipCommand(arguments, attempt: attempt + 1) continuation.resume(returning: result) } catch { continuation.resume(throwing: error) } } } else { let errorOutput = String(data: errorData, encoding: .utf8) ?? "Unknown error" continuation.resume(throwing: NSError(domain: "PipError", code: Int(process.terminationStatus), userInfo: [NSLocalizedDescriptionKey: errorOutput])) } } catch { continuation.resume(throwing: error) } } } } // MARK: - Python Detection private func detectActivePython() async -> String { let process = Process() process.executableURL = URL(fileURLWithPath: "/usr/bin/which") process.arguments = ["python3"] // Use user's environment to respect their PATH (e.g., Homebrew Python) process.environment = ProcessInfo.processInfo.userEnvironment let pipe = Pipe() process.standardOutput = pipe process.standardError = Pipe() do { try process.run() process.waitUntilExit() if process.terminationStatus == 0 { let data = pipe.fileHandleForReading.readDataToEndOfFile() let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" if !output.isEmpty { return output } } } catch { // Fallback to system Python on error } return "/usr/bin/python3" // Fallback } private func detectSitePackages(pythonPath: String) async -> [String] { let process = Process() process.executableURL = URL(fileURLWithPath: pythonPath) process.arguments = ["-c", "import site; print('\\n'.join(site.getsitepackages()))"] // Set environment, prioritizing the selected Python's directory var env = ProcessInfo.processInfo.userEnvironment let pythonDir = (pythonPath as NSString).deletingLastPathComponent if let currentPath = env["PATH"] { env["PATH"] = "\(pythonDir):\(currentPath)" } else { env["PATH"] = pythonDir } process.environment = env let pipe = Pipe() process.standardOutput = pipe process.standardError = Pipe() do { try process.run() process.waitUntilExit() if process.terminationStatus == 0 { let data = pipe.fileHandleForReading.readDataToEndOfFile() let output = String(data: data, encoding: .utf8) ?? "" let paths = output.components(separatedBy: "\n") .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .filter { !$0.isEmpty } return paths } } catch { // Silent error handling - return empty array } return [] } } struct PathLibrary { static func getPaths() -> [PathEnv] { return [ PathEnv(name: "Android Studio", paths: [ "~/.android/", "~/Library/Application Support/Google/AndroidStudio*/", "~/Library/Logs/AndroidStudio/", "~/Library/Caches/Google/AndroidStudio*/" ]), PathEnv(name: "Cargo", paths: [ "~/.cargo/", "~/.cargo/git/", "~/.cargo/registry/" ]), PathEnv(name: "Carthage", paths: [ "~/Carthage/", "~/Library/Caches/org.carthage.CarthageKit/" ]), PathEnv(name: "CocoaPods", paths: [ "~/Library/Caches/CocoaPods/", "~/.cocoapods/repos/" ]), PathEnv(name: "Composer", paths: [ "~/.composer/cache/" ]), PathEnv(name: "Conda", paths: [ "~/.conda/", "~/anaconda3/", "~/miniconda3/" ]), PathEnv(name: "Cursor", paths: [ "~/Library/Application Support/Cursor/", "~/Library/Application Support/Cursor/Cache", "~/Library/Application Support/Cursor/GPUCache", "~/Library/Application Support/Cursor/CachedConfigurations", "~/Library/Application Support/Cursor/CachedData", "~/Library/Application Support/Cursor/CachedExtensionVSIXs", "~/Library/Application Support/Cursor/CachedExtensions", "~/Library/Application Support/Cursor/CachedProfilesData", "~/Library/Application Support/Cursor/Code Cache", "~/Library/Application Support/Cursor/User", "~/.cursor/", "~/.cursor/extensions/" ]), PathEnv(name: "Deno", paths: [ "~/Library/Caches/deno" ]), PathEnv(name: "Go Modules", paths: [ "~/go/bin/", "~/go/pkg/mod/" ]), PathEnv(name: "Gradle", paths: [ "~/.gradle/caches/", "~/.gradle/wrapper/" ]), PathEnv(name: "Haskell Stack", paths: [ "~/.stack/", "~/.stack/global-project/", "~/.stack/snapshots/" ]), PathEnv(name: "IntelliJ IDEA", paths: [ "~/Library/Application Support/JetBrains/", "~/Library/Caches/JetBrains/", "~/Library/Logs/JetBrains/" ]), PathEnv(name: "Maven", paths: [ "~/.m2/" ]), PathEnv(name: "Nix", paths: [ "/nix/store/", "~/.cache/nix/" ]), PathEnv(name: "Npm", paths: [ "/usr/local/lib/node_modules/", "~/.nvm/versions/node/*/", "~/.npm/", "~/.nvm/", "~/Library/pnpm/store", "~/.bun/install/cache" ]), PathEnv(name: "Pip", paths: [ "~/Library/Caches/pip/" ]), PathEnv(name: "Poetry", paths: [ "~/Library/Caches/pypoetry/", "~/Library/Application Support/pypoetry/" ]), PathEnv(name: "Pub", paths: [ "~/.pub-cache/", "~/Library/Caches/flutter_engine/" ]), PathEnv(name: "Pyenv", paths: [ "~/.pyenv/", "~/.pyenv/cache/" ]), PathEnv(name: "Ruby Gems", paths: [ "~/.gem/", "~/.gem/ruby/*/" ]), PathEnv(name: "Swift", paths: [ "~/.swiftpm/" ]), PathEnv(name: "Uv", paths: [ "~/.cache/uv/", "~/.config/uv/", "~/.local/share/uv/" ]), PathEnv(name: "VS Code", paths: [ "~/Library/Application Support/Code/", "~/Library/Application Support/Code/Cache", "~/Library/Application Support/Code/GPUCache", "~/Library/Application Support/Code/CachedConfigurations", "~/Library/Application Support/Code/CachedData", "~/Library/Application Support/Code/CachedExtensionVSIXs", "~/Library/Application Support/Code/CachedExtensions", "~/Library/Application Support/Code/CachedProfilesData", "~/Library/Application Support/Code/Code Cache", "~/Library/Application Support/Code/User", "~/.vscode/", "~/.vscode/extensions/", "~/.vscode/cli/" ]), PathEnv(name: "Xcode", paths: [ "~/Library/Caches/com.apple.dt.xcodebuild/", "~/Library/Caches/com.apple.dt.Xcode.sourcecontrol.Git/", "~/Library/Developer/CoreSimulator/Devices/", "~/Library/Developer/DeveloperDiskImages/", "~/Library/Developer/Xcode/Archives/", "~/Library/Developer/Xcode/DerivedData/", "~/Library/Developer/Xcode/DocumentationCache/", "~/Library/Developer/Xcode/iOS DeviceSupport/", "~/Library/Developer/Xcode/tvOS DeviceSupport/", "~/Library/Developer/Xcode/watchOS DeviceSupport/", "~/Library/Developer/Xcode/macOS DeviceSupport/", "~/Library/Developer/Xcode/UserData/" ]), PathEnv(name: "Yarn", paths: [ "~/.cache/yarn/", "~/.yarn-cache/", "~/.yarn/global/" ]), PathEnv(name: "Zed", paths: [ "~/.config/zed/", "~/Library/Caches/Zed/", "~/Library/Application Support/Zed/", "~/Library/Application Support/Zed/node/cache/" ]) ] .map { PathEnv(name: $0.name, paths: $0.paths.sorted()) } // Sort paths within each environment .sorted { $0.name < $1.name } // Sort environments by name } } ================================================ FILE: Pearcleaner/Views/FileSearchView.swift ================================================ // // FileSearchView.swift // Pearcleaner // // Created by Alin Lupascu on 09/29/25. // import SwiftUI import AlinFoundation struct FileSearchView: View { @EnvironmentObject var appState: AppState @Environment(\.colorScheme) var colorScheme @ObservedObject private var consoleManager = GlobalConsoleManager.shared @State private var results: [FileSearchResult] = [] @State private var selectedResults: Set = [] @State private var selectedVolume: String = "/" @State private var selectedVolumeName: String = "Startup Disk" @State private var activeFilters: [FilterType] = [] @State private var isSearching: Bool = false @State private var includeSubfolders: Bool = true @State private var includeHiddenFiles: Bool = false @State private var caseSensitive: Bool = false @State private var excludeSystemFolders: Bool = true @State private var searchType: SearchType = .filesAndFolders @State private var currentSearcher: FileSearchEngine? @State private var sortOrder: [KeyPathComparator] = [.init(\.name, order: .forward)] @State private var calculatedFolderSizes: [URL: Int64] = [:] @State private var showAddFilterMenu: Bool = false @State private var showNameFilterDialog: Bool = false @State private var showExtensionFilterDialog: Bool = false @State private var showSizeFilterDialog: Bool = false @State private var showDateFilterDialog: Bool = false @State private var showTagsFilterDialog: Bool = false @State private var showCommentFilterDialog: Bool = false @State private var searchStartTime: Date? @State private var searchElapsedTime: TimeInterval = 0 @State private var elapsedTimeUpdateTimer: Timer? @State private var hasSearched: Bool = false @State private var filterText: String = "" @State private var editingItemId: UUID? @State private var editingText: String = "" @State private var deletedItemsCache: [FileSearchResult] = [] @FocusState private var isEditingFocused: Bool @AppStorage("settings.interface.scrollIndicators") private var scrollIndicators: Bool = false @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true private var sortedResults: [FileSearchResult] { let sorted = results.sorted(using: sortOrder) // Apply filter text if present if filterText.isEmpty { return sorted } else { return sorted.filter { result in result.name.localizedCaseInsensitiveContains(filterText) || result.url.path.localizedCaseInsensitiveContains(filterText) } } } var body: some View { VStack(alignment: .leading, spacing: 0) { // Filter controls VStack(alignment: .leading, spacing: 12) { HStack { // Volume selector Menu { Button("Startup Disk") { selectedVolume = "/" selectedVolumeName = "Startup Disk" } if !appState.volumeInfos.isEmpty { Divider() ForEach(appState.volumeInfos) { volume in Button(volume.name) { selectedVolume = volume.path selectedVolumeName = volume.name } } } Divider() Button("Choose Folder...") { let panel = NSOpenPanel() panel.canChooseFiles = false panel.canChooseDirectories = true panel.allowsMultipleSelection = false panel.prompt = "Choose" if panel.runModal() == .OK, let url = panel.url { selectedVolume = url.path selectedVolumeName = url.lastPathComponent } } } label: { HStack { Image(systemName: "externaldrive") Text(selectedVolumeName) Image(systemName: "chevron.down") .font(.caption) } } .buttonStyle(ControlGroupButtonStyle( foregroundColor: ThemeColors.shared(for: colorScheme).accent, shape: Capsule(style: .continuous), level: .primary, skipControlGroup: true )) // Search type selector Menu { ForEach(SearchType.allCases, id: \.self) { type in Button(type.rawValue) { searchType = type // Disable subfolders option when files only, enable for others if type == .filesOnly { includeSubfolders = false } else { includeSubfolders = true } } } } label: { HStack { Image(systemName: searchType == .filesOnly ? "doc" : searchType == .foldersOnly ? "folder" : "doc.on.doc") Text(searchType.rawValue) Image(systemName: "chevron.down") .font(.caption) } } .buttonStyle(ControlGroupButtonStyle( foregroundColor: ThemeColors.shared(for: colorScheme).primaryText, shape: Capsule(style: .continuous), level: .primary, skipControlGroup: true )) // Search options Toggle("Include subfolders", isOn: $includeSubfolders) .toggleStyle(.checkbox) .controlSize(.small) .disabled(searchType == .filesOnly) Toggle("Exclude system folders", isOn: $excludeSystemFolders) .toggleStyle(.checkbox) .controlSize(.small) Toggle("Include hidden files", isOn: $includeHiddenFiles) .toggleStyle(.checkbox) .controlSize(.small) Toggle("Case sensitive", isOn: $caseSensitive) .toggleStyle(.checkbox) .controlSize(.small) Spacer() } // Active filters if !activeFilters.isEmpty { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { ForEach(Array(activeFilters.enumerated()), id: \.element.id) { index, filter in FilterChip(filter: filter, onUpdate: { updatedFilter in activeFilters[index] = updatedFilter }, onRemove: { removeFilter(filter) }) } } } } } .padding(.horizontal, 20) .padding(.top, 5) // Stats header if hasSearched || isSearching { HStack(spacing: 12) { // Results count - leading aligned, 200 width Text("\(results.count) result\(results.count == 1 ? "" : "s")") .font(.caption) .monospacedDigit() .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .frame(width: 200, alignment: .leading) Spacer() // Action buttons - centered Button(action: { if selectedResults.count == 1, let selectedId = selectedResults.first, let result = results.first(where: { $0.id == selectedId }) { NSWorkspace.shared.open(result.url) } }) { Label("Open", systemImage: "arrow.up.forward.app") .font(.caption) } .buttonStyle(.plain) .disabled(selectedResults.count != 1) Button(action: { if selectedResults.count == 1, let selectedId = selectedResults.first, let result = results.first(where: { $0.id == selectedId }) { // Temporarily deselect to force re-render, then reselect selectedResults.removeAll() DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { editingItemId = selectedId editingText = result.name selectedResults.insert(selectedId) DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { isEditingFocused = true } } } }) { Label("Rename", systemImage: "pencil") .font(.caption) } .buttonStyle(.plain) .disabled(selectedResults.count != 1) Button(action: { deleteSelectedItems() }) { Label(selectedResults.count > 1 ? "Bulk Delete" : "Delete", systemImage: "trash") .font(.caption) .foregroundStyle(.red) } .buttonStyle(.plain) .disabled(selectedResults.isEmpty) Spacer() // Elapsed time - trailing aligned, 200 width if searchStartTime != nil { Text("Elapsed Time: \(formatElapsedTime(searchElapsedTime))") .font(.caption) .monospacedDigit() .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .frame(width: 200, alignment: .trailing) } else { EmptyView() .frame(width: 200, alignment: .trailing) } } .padding(.horizontal, 20) .padding(.vertical) } // Results table if results.isEmpty && !isSearching { VStack { Spacer() if hasSearched { Text("No results") .font(.title2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text("Try adjusting your filters") .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } else { Text("Ready to search") .font(.title2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text("Add filters and click Play to find files") .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } Spacer() } .frame(maxWidth: .infinity) } else { Table(sortedResults, selection: $selectedResults, sortOrder: $sortOrder) { TableColumn("") { result in HStack { Spacer() Button(action: { toggleSelection(for: result) }) { Image(systemName: selectedResults.contains(result.id) ? "checkmark.square.fill" : "square") .foregroundStyle(selectedResults.contains(result.id) ? .blue : ThemeColors.shared(for: colorScheme).secondaryText) } .buttonStyle(.plain) Spacer() } } .width(40) TableColumn("") { result in HStack { Spacer() if let icon = result.icon { Image(nsImage: icon) .resizable() .scaledToFit() .frame(width: 24, height: 24) } Spacer() } } .width(40) TableColumn("Name", value: \.name) { result in if editingItemId == result.id { TextField(LocalizedStringKey(""), text: $editingText, onCommit: { if !editingText.isEmpty && editingText != result.name { performRename(result: result, newName: editingText) } editingItemId = nil isEditingFocused = false }) .textFieldStyle(.plain) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .focused($isEditingFocused) .id(result.id) } else { Text(result.name) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } } .width(min: 150, ideal: 250) TableColumn("Type", value: \.type) { result in Text(result.type) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .width(80) TableColumn("Size", value: \.size) { result in SizeCell(result: result, calculatedFolderSizes: $calculatedFolderSizes) } .width(100) TableColumn("Modified", value: \.dateModified) { result in Text(formatDate(result.dateModified)) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .width(150) } .onDeleteCommand { if !selectedResults.isEmpty { deleteSelectedItems() } } .contextMenu(forSelectionType: FileSearchResult.ID.self) { items in if let firstId = items.first, let result = results.first(where: { $0.id == firstId }) { Button("Rename") { selectedResults.removeAll() DispatchQueue.main.asyncAfter(deadline: .now() + 0.01) { editingItemId = firstId editingText = result.name selectedResults.insert(firstId) DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { isEditingFocused = true } } } Button("View in Finder") { NSWorkspace.shared.activateFileViewerSelecting([result.url]) } Button("Copy Path") { NSPasteboard.general.clearContents() NSPasteboard.general.setString(result.url.path, forType: .string) } Divider() Button("Delete", role: .destructive) { deleteFile(result) } } } .scrollIndicators(scrollIndicators ? .automatic : .never) } } .frame(maxWidth: .infinity, maxHeight: .infinity) .safeAreaInset(edge: .bottom) { // Bottom status bar - always visible when there are results if !results.isEmpty || isSearching { HStack(spacing: 12) { // Select-all text button on far left Button(action: { if selectedResults.count == results.count { selectedResults.removeAll() } else { selectedResults = Set(results.map { $0.id }) } }) { Text(selectedResults.count == results.count && !results.isEmpty ? "Deselect All" : "Select All") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .buttonStyle(.plain) .padding(.leading, 4) // Path when single item selected if selectedResults.count == 1, let selectedId = selectedResults.first, let selectedResult = results.first(where: { $0.id == selectedId }) { Divider() .frame(height: 12) PathStatusBar(url: selectedResult.url) } else { Spacer() } // Filter field and searching indicator on the right HStack(spacing: 8) { if isSearching { ProgressView() .controlSize(.small) } HStack(spacing: 6) { Image(systemName: "magnifyingglass") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) TextField("Filter", text: $filterText) .textFieldStyle(.plain) .font(.caption) .frame(width: 150) if !filterText.isEmpty { Button(action: { filterText = "" }) { Image(systemName: "xmark.circle.fill") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .buttonStyle(.plain) } } .padding(.horizontal, 8) .padding(.vertical, 4) .background(ThemeColors.shared(for: colorScheme).secondaryBG) .cornerRadius(6) } } .padding(.horizontal, 12) .padding(.vertical, 8) .background(ThemeColors.shared(for: colorScheme).primaryBG) } } .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("FileSearchViewShouldUndo"))) { _ in // Restore deleted items from cache after undo if !deletedItemsCache.isEmpty { results.append(contentsOf: deletedItemsCache) deletedItemsCache.removeAll() } } .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("FileSearchViewShouldRefresh"))) { _ in // Re-run search with current parameters if a search has been performed if hasSearched { startSearch() } } .toolbarBackground(.hidden, for: .windowToolbar) .toolbar { TahoeToolbarItem(placement: .navigation) { HStack { VStack(alignment: .leading) { Text("File Search") .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .font(.title2) .fontWeight(.bold) Text("Search for files and folders with advanced filters") .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } } ToolbarItem { Spacer() } TahoeToolbarItem(isGroup: true) { Button { withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) { consoleManager.showConsole.toggle() } } label: { Label("Console", systemImage: consoleManager.showConsole ? "terminal.fill" : "terminal") } .help("Toggle console output") Menu { Button("Name") { showNameFilterDialog = true } Button("Extension") { showExtensionFilterDialog = true } Button("Size") { showSizeFilterDialog = true } Button("Date") { showDateFilterDialog = true } Button("Tags") { showTagsFilterDialog = true } Button("Comment") { showCommentFilterDialog = true } Menu("Kind") { ForEach(KindFilterType.allCases, id: \.self) { kind in Button(kind.displayName) { addFilter(.kind(kind)) } } } } label: { Label("Add Filter", systemImage: "line.3.horizontal.decrease.circle") } .labelStyle(.titleAndIcon) .menuIndicator(.hidden) Button { results.removeAll() selectedResults.removeAll() hasSearched = false searchStartTime = nil searchElapsedTime = 0 deletedItemsCache.removeAll() activeFilters.removeAll() } label: { Label("Clear", systemImage: "trash") } .disabled(results.isEmpty) if isSearching { Button { stopSearch() } label: { Label("Stop", systemImage: "stop.fill") } } else { Button { startSearch() } label: { Label("Search", systemImage: "play.fill") } } } } .sheet(isPresented: $showNameFilterDialog) { NameFilterDialog { type, value in addFilter(.name(type, value)) showNameFilterDialog = false } onCancel: { showNameFilterDialog = false } } .sheet(isPresented: $showExtensionFilterDialog) { ExtensionFilterDialog { type, value in addFilter(.fileExtension(type, value)) showExtensionFilterDialog = false } onCancel: { showExtensionFilterDialog = false } } .sheet(isPresented: $showSizeFilterDialog) { SizeFilterDialog { type, value, max in addFilter(.size(type, value, max)) showSizeFilterDialog = false } onCancel: { showSizeFilterDialog = false } } .sheet(isPresented: $showDateFilterDialog) { DateFilterDialog { type, value, end in addFilter(.date(type, value, end)) showDateFilterDialog = false } onCancel: { showDateFilterDialog = false } } .sheet(isPresented: $showTagsFilterDialog) { TagsFilterDialog { type, value in addFilter(.tags(type, value)) showTagsFilterDialog = false } onCancel: { showTagsFilterDialog = false } } .sheet(isPresented: $showCommentFilterDialog) { CommentFilterDialog { type, value in addFilter(.comment(type, value)) showCommentFilterDialog = false } onCancel: { showCommentFilterDialog = false } } } // MARK: - Helper Functions private func toggleSelection(for result: FileSearchResult) { if selectedResults.contains(result.id) { selectedResults.remove(result.id) } else { selectedResults.insert(result.id) } } private func addFilter(_ filter: FilterType) { activeFilters.append(filter) } private func removeFilter(_ filter: FilterType) { activeFilters.removeAll { $0.id == filter.id } } private func startSearch() { GlobalConsoleManager.shared.appendOutput("Starting file search in \(selectedVolumeName)...\n", source: CurrentPage.fileSearch.title) isSearching = true hasSearched = true results.removeAll() selectedResults.removeAll() // Start elapsed time tracking searchStartTime = Date() searchElapsedTime = 0 // Start a timer to update elapsed time every 0.1 seconds elapsedTimeUpdateTimer?.invalidate() elapsedTimeUpdateTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in if let startTime = searchStartTime { searchElapsedTime = Date().timeIntervalSince(startTime) } } let searcher = FileSearchEngine() currentSearcher = searcher searcher.search( rootPath: selectedVolume, filters: activeFilters, includeSubfolders: includeSubfolders, includeHiddenFiles: includeHiddenFiles, caseSensitive: caseSensitive, searchType: searchType, excludeSystemFolders: excludeSystemFolders, onBatchFound: { batch in results.append(contentsOf: batch) }, completion: { isSearching = false currentSearcher = nil GlobalConsoleManager.shared.appendOutput("✓ File search completed - found \(results.count) result(s)\n", source: CurrentPage.fileSearch.title) // Stop the timer but keep the final elapsed time self.elapsedTimeUpdateTimer?.invalidate() self.elapsedTimeUpdateTimer = nil } ) } private func stopSearch() { GlobalConsoleManager.shared.appendOutput("Stopping file search...\n", source: CurrentPage.fileSearch.title) currentSearcher?.stop() isSearching = false currentSearcher = nil GlobalConsoleManager.shared.appendOutput("✓ File search stopped\n", source: CurrentPage.fileSearch.title) // Stop the timer but keep the elapsed time elapsedTimeUpdateTimer?.invalidate() elapsedTimeUpdateTimer = nil } private func performActionOnSelected(action: (FileSearchResult) -> Void) { let itemsToAction = results.filter { selectedResults.contains($0.id) } for item in itemsToAction { action(item) } } private func performRename(result: FileSearchResult, newName: String) { let newURL = result.url.deletingLastPathComponent().appendingPathComponent(newName) // Always use helper for file operations (works for all file types) let command = "/bin/mv \"\(result.url.path)\" \"\(newURL.path)\"" Task { let moveResult = try! await runSUCommand( command, errorContext: "Failed to rename file", throwOnFailure: false ) if moveResult.0 { // Update the result in the list if let index = results.firstIndex(where: { $0.id == result.id }) { let updatedResult = FileSearchResult( url: newURL, name: newName, type: result.type, size: result.size, dateModified: result.dateModified, isDirectory: result.isDirectory, icon: result.icon ) results[index] = updatedResult } } else { let error = NSError(domain: "com.pearcleaner.rename", code: 1, userInfo: [NSLocalizedDescriptionKey: "Failed to rename file"]) showCustomAlert( title: "Rename Failed", message: "Failed to rename '\(result.name)' to '\(newName)'. Error: \(error.localizedDescription)", style: .critical ) } } } private func deleteFile(_ result: FileSearchResult) { Task { GlobalConsoleManager.shared.appendOutput("Starting deletion of \(result.name)...\n", source: CurrentPage.fileSearch.title) let success = await withCheckedContinuation { continuation in DispatchQueue.global(qos: .userInitiated).async { let success = FileManagerUndo.shared.deleteFiles(at: [result.url], bundleName: "File Search - \(result.name)") continuation.resume(returning: success) } } await MainActor.run { if success { GlobalConsoleManager.shared.appendOutput("✓ Completed deletion of \(result.name)\n", source: CurrentPage.fileSearch.title) // Cache the deleted item before removing deletedItemsCache.append(result) results.removeAll { $0.id == result.id } selectedResults.remove(result.id) } else { showCustomAlert( title: "Deletion Failed", message: "Failed to delete '\(result.name)'. The file may require additional permissions or may not exist.", style: .critical ) } } } } private func deleteSelectedItems() { let itemsToDelete = results.filter { selectedResults.contains($0.id) } let urlsToDelete = itemsToDelete.map { $0.url } Task { GlobalConsoleManager.shared.appendOutput("Starting deletion of \(itemsToDelete.count) selected file(s)...\n", source: CurrentPage.fileSearch.title) let success = await withCheckedContinuation { continuation in DispatchQueue.global(qos: .userInitiated).async { let bundleName = "File Search (\(itemsToDelete.count) items)" let success = FileManagerUndo.shared.deleteFiles(at: urlsToDelete, bundleName: bundleName) continuation.resume(returning: success) } } await MainActor.run { if success { GlobalConsoleManager.shared.appendOutput("✓ Completed deletion of \(itemsToDelete.count) file(s)\n", source: CurrentPage.fileSearch.title) // Cache the deleted items before removing deletedItemsCache.append(contentsOf: itemsToDelete) let deletedIds = Set(itemsToDelete.map { $0.id }) results.removeAll { deletedIds.contains($0.id) } selectedResults.removeAll() } else { showCustomAlert( title: "Deletion Failed", message: "Failed to delete some selected files. They may require additional permissions or may not exist.", style: .critical ) } } } } private func formatBytes(_ bytes: Int64) -> String { let formatter = ByteCountFormatter() formatter.allowsNonnumericFormatting = false formatter.countStyle = .file return formatter.string(fromByteCount: bytes) } private func formatDate(_ date: Date) -> String { let formatter = DateFormatter() formatter.dateStyle = .short formatter.timeStyle = .short return formatter.string(from: date) } private func formatElapsedTime(_ timeInterval: TimeInterval) -> String { let minutes = Int(timeInterval) / 60 let seconds = Int(timeInterval) % 60 if minutes > 0 { return String(format: "%d:%02d", minutes, seconds) } else { return String(format: "0:%02d", seconds) } } } // MARK: - Filter Chip View struct FilterChip: View { let filter: FilterType let onUpdate: (FilterType) -> Void let onRemove: () -> Void @Environment(\.colorScheme) var colorScheme @State private var editingText: String = "" @State private var isEditing: Bool = false var body: some View { HStack(spacing: 6) { // For name filters, make them editable if case .name(let type, let value) = filter { if isEditing { TextField(LocalizedStringKey(""), text: $editingText, onCommit: { onUpdate(.name(type, editingText)) isEditing = false }) .textFieldStyle(.plain) .font(.caption) .frame(minWidth: 60, maxWidth: 200) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } else { Text(filter.displayText) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .onTapGesture { editingText = value isEditing = true } } } else { Text(filter.displayText) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } Button(action: onRemove) { Image(systemName: "xmark.circle.fill") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .buttonStyle(.plain) } .padding(.horizontal, 10) .padding(.vertical, 4) .background( Capsule() .fill(ThemeColors.shared(for: colorScheme).secondaryBG) ) } } // MARK: - Filter Dialog Views struct NameFilterDialog: View { let onAdd: (NameFilterType, String) -> Void let onCancel: () -> Void @State private var selectedType: NameFilterType = .contains @State private var value: String = "" @Environment(\.colorScheme) var colorScheme var body: some View { VStack(spacing: 20) { Text("Add Name Filter") .font(.headline) Picker("Filter Type", selection: $selectedType) { ForEach(NameFilterType.allCases, id: \.self) { type in Text(type.displayName).tag(type) } } TextField("Value", text: $value) .textFieldStyle(.roundedBorder) HStack { Button("Cancel") { onCancel() } .keyboardShortcut(.cancelAction) Button("Add") { if !value.isEmpty { onAdd(selectedType, value) } } .keyboardShortcut(.defaultAction) .disabled(value.isEmpty) } } .padding() .frame(width: 300) } } struct ExtensionFilterDialog: View { let onAdd: (ExtensionFilterType, String) -> Void let onCancel: () -> Void @State private var selectedType: ExtensionFilterType = .includes @State private var value: String = "" @Environment(\.colorScheme) var colorScheme var body: some View { VStack(spacing: 20) { Text("Add Extension Filter") .font(.headline) Picker("Filter Type", selection: $selectedType) { ForEach(ExtensionFilterType.allCases, id: \.self) { type in Text(type.displayName).tag(type) } } TextField("Extensions (comma-separated, e.g., jpg,png,pdf)", text: $value) .textFieldStyle(.roundedBorder) HStack { Button("Cancel") { onCancel() } .keyboardShortcut(.cancelAction) Button("Add") { if !value.isEmpty { onAdd(selectedType, value) } } .keyboardShortcut(.defaultAction) .disabled(value.isEmpty) } } .padding() .frame(width: 400) } } struct SizeFilterDialog: View { let onAdd: (SizeFilterType, Int64, Int64?) -> Void let onCancel: () -> Void @State private var selectedType: SizeFilterType = .greaterThan @State private var value: String = "" @State private var maxValue: String = "" @State private var unit: Int64 = 1_048_576 // MB by default @Environment(\.colorScheme) var colorScheme var body: some View { VStack(spacing: 20) { Text("Add Size Filter") .font(.headline) Picker("Filter Type", selection: $selectedType) { ForEach(SizeFilterType.allCases, id: \.self) { type in Text(type.displayName).tag(type) } } HStack { TextField("Value", text: $value) .textFieldStyle(.roundedBorder) Picker("Unit", selection: $unit) { Text("KB").tag(Int64(1_024)) Text("MB").tag(Int64(1_048_576)) Text("GB").tag(Int64(1_073_741_824)) } .frame(width: 80) } if selectedType == .between { HStack { TextField("Max Value", text: $maxValue) .textFieldStyle(.roundedBorder) Text("(same unit)") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } HStack { Button("Cancel") { onCancel() } .keyboardShortcut(.cancelAction) Button("Add") { if let numValue = Int64(value) { let bytes = numValue * unit let maxBytes = selectedType == .between ? (Int64(maxValue) ?? 0) * unit : nil onAdd(selectedType, bytes, maxBytes) } } .keyboardShortcut(.defaultAction) .disabled(value.isEmpty || (selectedType == .between && maxValue.isEmpty)) } } .padding() .frame(width: 350) } } struct DateFilterDialog: View { let onAdd: (DateFilterType, Date, Date?) -> Void let onCancel: () -> Void @State private var selectedType: DateFilterType = .modifiedAfter @State private var date: Date = Date() @State private var endDate: Date = Date() @Environment(\.colorScheme) var colorScheme var body: some View { VStack(spacing: 20) { Text("Add Date Filter") .font(.headline) Picker("Filter Type", selection: $selectedType) { ForEach(DateFilterType.allCases, id: \.self) { type in Text(type.displayName).tag(type) } } DatePicker("Date", selection: $date, displayedComponents: [.date]) if selectedType == .createdBetween || selectedType == .modifiedBetween { DatePicker("End Date", selection: $endDate, displayedComponents: [.date]) } HStack { Button("Cancel") { onCancel() } .keyboardShortcut(.cancelAction) Button("Add") { let end = (selectedType == .createdBetween || selectedType == .modifiedBetween) ? endDate : nil onAdd(selectedType, date, end) } .keyboardShortcut(.defaultAction) } } .padding() .frame(width: 300) } } struct TagsFilterDialog: View { let onAdd: (TagFilterType, String) -> Void let onCancel: () -> Void @State private var selectedType: TagFilterType = .hasTag @State private var value: String = "" @Environment(\.colorScheme) var colorScheme var body: some View { VStack(spacing: 20) { Text("Add Tags Filter") .font(.headline) Picker("Filter Type", selection: $selectedType) { ForEach(TagFilterType.allCases, id: \.self) { type in Text(type.displayName).tag(type) } } if selectedType != .hasAnyOfTags && selectedType != .hasAllOfTags { TextField("Tag name", text: $value) .textFieldStyle(.roundedBorder) } else { TextField("Tag names (comma-separated)", text: $value) .textFieldStyle(.roundedBorder) } HStack { Button("Cancel") { onCancel() } .keyboardShortcut(.cancelAction) Button("Add") { if !value.isEmpty { onAdd(selectedType, value) } } .keyboardShortcut(.defaultAction) .disabled(value.isEmpty) } } .padding() .frame(width: 350) } } struct CommentFilterDialog: View { let onAdd: (CommentFilterType, String) -> Void let onCancel: () -> Void @State private var selectedType: CommentFilterType = .contains @State private var value: String = "" @Environment(\.colorScheme) var colorScheme var body: some View { VStack(spacing: 20) { Text("Add Comment Filter") .font(.headline) Picker("Filter Type", selection: $selectedType) { ForEach(CommentFilterType.allCases, id: \.self) { type in Text(type.displayName).tag(type) } } if selectedType != .isEmpty { TextField("Value", text: $value) .textFieldStyle(.roundedBorder) } HStack { Button("Cancel") { onCancel() } .keyboardShortcut(.cancelAction) Button("Add") { onAdd(selectedType, value) } .keyboardShortcut(.defaultAction) .disabled(selectedType != .isEmpty && value.isEmpty) } } .padding() .frame(width: 350) } } // MARK: - Size Cell with Lazy Folder Size Calculation struct SizeCell: View { let result: FileSearchResult @Binding var calculatedFolderSizes: [URL: Int64] @Environment(\.colorScheme) var colorScheme @State private var isCalculating: Bool = false var body: some View { HStack(spacing: 4) { if result.isDirectory { // For folders, calculate size on demand if let cachedSize = calculatedFolderSizes[result.url] { Text(formatBytes(cachedSize)) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } else if isCalculating { ProgressView() .controlSize(.mini) .scaleEffect(0.7) } else { ProgressView() .controlSize(.mini) .scaleEffect(0.7) .onAppear { calculateFolderSize() } } } else { // For files, show the size immediately Text(formatBytes(result.size)) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } } private func calculateFolderSize() { isCalculating = true Task.detached(priority: .utility) { let size = totalSizeOnDisk(for: result.url) await MainActor.run { calculatedFolderSizes[result.url] = size isCalculating = false } } } private func formatBytes(_ bytes: Int64) -> String { let formatter = ByteCountFormatter() formatter.allowsNonnumericFormatting = false formatter.countStyle = .file return formatter.string(fromByteCount: bytes) } } // MARK: - Path Status Bar struct PathStatusBar: View { let url: URL @Environment(\.colorScheme) var colorScheme var body: some View { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 4) { ForEach(Array(pathComponents.enumerated()), id: \.offset) { index, component in Button(action: { openPathComponent(at: index) }) { Text(component.name) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } .buttonStyle(.plain) .help(component.path) if index < pathComponents.count - 1 { Image(systemName: "chevron.right") .font(.caption2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } } } } private var pathComponents: [(name: String, path: String)] { let path = url.path var components: [(String, String)] = [] var currentPath = "" let parts = path.components(separatedBy: "/").filter { !$0.isEmpty } // Add root components.append(("/", "/")) currentPath = "/" // Add each component for part in parts { currentPath += (currentPath == "/" ? "" : "/") + part components.append((part, currentPath)) } return components } private func openPathComponent(at index: Int) { let component = pathComponents[index] let componentURL = URL(fileURLWithPath: component.path) NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: componentURL.path) } } ================================================ FILE: Pearcleaner/Views/FilesView/FileCategory.swift ================================================ // // FileCategory.swift // Pearcleaner // // Created by Alin Lupascu on 11/18/25. // import Foundation import SwiftUI import AppKit import AlinFoundation // MARK: - FileCategory Enum enum FileCategory: String, CaseIterable, Identifiable { case application = "Application Bundles" case preferences = "Preferences" case caches = "Caches" case applicationSupport = "Application Support" case containers = "Containers" case logs = "Logs" case launchAgents = "Launch Agents & Daemons" case savedState = "Saved Application State" case internetPlugins = "Internet Plug-Ins" case applicationScripts = "Application Scripts" case systemFiles = "System Files" case userFiles = "User Files" case other = "Other" var id: String { rawValue } var icon: String { switch self { case .application: return "app.fill" case .preferences: return "gearshape.fill" case .caches: return "tray.fill" case .applicationSupport: return "folder.fill" case .containers: return "cube.box.fill" case .logs: return "doc.text.fill" case .launchAgents: return "gearshape.2.fill" case .savedState: return "clock.arrow.circlepath" case .internetPlugins: return "network" case .applicationScripts: return "applescript.fill" case .systemFiles: return "cpu.fill" case .userFiles: return "person.fill" case .other: return "questionmark.folder.fill" } } var sortOrder: Int { // Application bundle should always be first return FileCategory.allCases.firstIndex(of: self) ?? 100 } } // MARK: - Categorization Function func categorizeFile(_ url: URL) -> FileCategory { let path = url.path // Check if it's an app bundle (should be first) if url.pathExtension == "app" { return .application } // Check preferences if path.contains("/Library/Preferences") { return .preferences } // Check caches if path.contains("/Library/Caches") { return .caches } // Check application support if path.contains("/Library/Application Support") { return .applicationSupport } // Check containers if path.contains("/Library/Containers") || path.contains("/Library/Group Containers") { return .containers } // Check logs if path.contains("/Library/Logs") { return .logs } // Check launch agents/daemons if path.contains("/LaunchAgents") || path.contains("/LaunchDaemons") { return .launchAgents } // Check saved application state if path.contains("/Saved Application State") { return .savedState } // Check internet plug-ins if path.contains("/Internet Plug-Ins") { return .internetPlugins } // Check application scripts if path.contains("/Application Scripts") { return .applicationScripts } // Check system files if path.contains("/Library/Extensions") || path.contains("/Library/PrivilegedHelperTools") || path.contains("/private/var/db/receipts") || path.contains("/HTTPStorages") || path.contains("/Library/WebKit") { return .systemFiles } // Check user files (anything in home directory, excluding ~/Applications) if path.hasPrefix(home) && !path.contains("\(home)/Applications") { return .userFiles } // Default to other return .other } // MARK: - GroupedFiles Struct struct GroupedFiles { let category: FileCategory var files: [URL] var totalSize: Int64 var isExpanded: Bool // Selection state var allSelected: Bool // All files in category selected var someSelected: Bool // Some (but not all) files selected } // MARK: - FileCategoryView Component struct FileCategoryView: View { @EnvironmentObject var appState: AppState @Environment(\.colorScheme) var colorScheme @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true let group: GroupedFiles let onToggleExpand: () -> Void let onToggleSelection: () -> Void let fileItemBinding: (URL) -> Binding let removeAssociation: (URL) -> Void var body: some View { VStack(alignment: .leading, spacing: 0) { // Category Header HStack(spacing: 10) { // Expand/Collapse chevron Button(action: onToggleExpand) { Image(systemName: group.isExpanded ? "chevron.down" : "chevron.right") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .frame(width: 10) } .buttonStyle(.plain) // Category checkbox (select all in category) Button(action: onToggleSelection) { Image(systemName: group.allSelected ? "checkmark.circle.fill" : (group.someSelected ? "circle.lefthalf.filled" : "circle")) .foregroundStyle(group.allSelected || group.someSelected ? ThemeColors.shared(for: colorScheme).accent : ThemeColors.shared(for: colorScheme).secondaryText) .font(.title3) } .buttonStyle(.plain) .help(group.allSelected ? "Deselect all files in this category" : "Select all files in this category") // Category name Text(group.category.rawValue) .font(.headline) .fontWeight(.semibold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) // File count Text(verbatim: "(\(group.files.count))") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() // Total size Text(formatByte(size: group.totalSize).human) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .padding(.vertical, 8) .contentShape(Rectangle()) .onTapGesture { onToggleExpand() } // Files in category (only if expanded) if group.isExpanded { LazyVStack(spacing: 0) { ForEach(Array(group.files.enumerated()), id: \.element) { index, path in VStack(spacing: 0) { FileDetailsItem( path: path, removeAssociation: removeAssociation, isSelected: fileItemBinding(path) ) .padding(.leading, 35) // Indent files under category if index < group.files.count - 1 { Divider() .padding(.leading, 35) } } } } } } } } // MARK: - ZombieFileCategoryView Component struct ZombieFileCategoryView: View { @EnvironmentObject var appState: AppState @Environment(\.colorScheme) var colorScheme @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true let group: GroupedFiles let onToggleExpand: () -> Void let onToggleSelection: () -> Void let fileItemBinding: (URL) -> Binding @Binding var memoizedFiles: [URL] var body: some View { VStack(alignment: .leading, spacing: 0) { // Category Header HStack(spacing: 10) { // Expand/Collapse chevron Button(action: onToggleExpand) { Image(systemName: group.isExpanded ? "chevron.down" : "chevron.right") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .frame(width: 10) } .buttonStyle(.plain) // Category checkbox (select all in category) Button(action: onToggleSelection) { Image(systemName: group.allSelected ? "checkmark.circle.fill" : (group.someSelected ? "circle.lefthalf.filled" : "circle")) .foregroundStyle(group.allSelected || group.someSelected ? ThemeColors.shared(for: colorScheme).accent : ThemeColors.shared(for: colorScheme).secondaryText) .font(.title3) } .buttonStyle(.plain) .help(group.allSelected ? "Deselect all files in this category" : "Select all files in this category") // Category name Text(group.category.rawValue) .font(.headline) .fontWeight(.semibold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) // File count Text(verbatim: "(\(group.files.count))") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() // Total size Text(formatByte(size: group.totalSize).human) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .padding(.vertical, 8) .contentShape(Rectangle()) .onTapGesture { onToggleExpand() } // Files in category (only if expanded) if group.isExpanded { LazyVStack(spacing: 0) { ForEach(Array(group.files.enumerated()), id: \.element) { index, path in VStack(spacing: 0) { if let fileSize = appState.zombieFile.fileSize[path], let fileIcon = appState.zombieFile.fileIcon[path], let iconImage = fileIcon.map(Image.init(nsImage:)) { ZombieFileDetailsItem( size: fileSize, icon: iconImage, path: path, memoizedFiles: $memoizedFiles, isSelected: fileItemBinding(path) ) .padding(.leading, 35) // Indent files under category } if index < group.files.count - 1 { Divider() .padding(.leading, 35) } } } } } } } } ================================================ FILE: Pearcleaner/Views/FilesView/FileListView.swift ================================================ // // FileListView.swift // Pearcleaner // // Created by Alin Lupascu on 7/31/25. // import AlinFoundation import Foundation import SwiftUI enum FileListViewMode: String { case simple = "Simple" case categorized = "Categorized" } struct FileListView: View { @AppStorage("settings.interface.scrollIndicators") private var scrollIndicators: Bool = false @AppStorage("settings.general.brew") private var brew: Bool = false @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true @EnvironmentObject var appState: AppState @Environment(\.colorScheme) var colorScheme @Binding var sortedFiles: [URL] @Binding var infoSidebar: Bool @Binding var selectedSort: SortOptionList @Binding var viewMode: FileListViewMode @State private var searchText: String = "" @State private var selectedFileItemsLocal: Set = [] @State private var memoizedFiles: [URL] = [] @State private var lastRefreshDate: Date? @State private var collapsedCategories: Set = [] let locations: Locations let windowController: WindowManager let handleUninstallAction: () -> Void let displaySizeTotal: String let totalSelectedSize: String let updateSortedFiles: () -> Void let removeSingleZombieAssociation: (URL) -> Void let removePath: (URL) -> Void var filteredFiles: [URL] { if searchText.isEmpty { return memoizedFiles } else { return memoizedFiles.filter { path in path.lastPathComponent.localizedCaseInsensitiveContains(searchText) || path.path.localizedCaseInsensitiveContains(searchText) } } } // Categorized files grouped by category var categorizedFiles: [GroupedFiles] { // Group files by category var grouped: [FileCategory: [URL]] = [:] for file in filteredFiles { let category = categorizeFile(file) grouped[category, default: []].append(file) } // Convert to GroupedFiles array return grouped.map { category, files in let totalSize = files.reduce(0) { sum, url in sum + (appState.appInfo.fileSize[url] ?? 0) } let selectedCount = files.filter { selectedFileItemsLocal.contains($0) }.count return GroupedFiles( category: category, files: files, totalSize: totalSize, isExpanded: !collapsedCategories.contains(category), allSelected: selectedCount == files.count && files.count > 0, someSelected: selectedCount > 0 && selectedCount < files.count ) } .sorted { $0.category.sortOrder < $1.category.sortOrder } } var body: some View { VStack(spacing: 0) { if appState.appInfo.fileSize.keys.count == 0 && !appState.isBrewCleanupInProgress { VStack { Spacer() Text(appState.externalMode ? "Sentinel Monitor found no other files to remove" : "There are no files to remove") .font(.title2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() } .frame(maxWidth: .infinity) } else if appState.isBrewCleanupInProgress && appState.appInfo.fileSize.keys.count == 0 { VStack { Spacer() HStack(spacing: 12) { ProgressView() Text("Running homebrew cleanup") .font(.title2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } Spacer() } .frame(maxWidth: .infinity) } else { VStack(alignment: .leading, spacing: 0) { // Search bar HStack { Image(systemName: "magnifyingglass") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) TextField("Search...", text: $searchText) .textFieldStyle(.plain) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) if !searchText.isEmpty { Button { searchText = "" } label: { Image(systemName: "xmark.circle.fill") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .buttonStyle(.plain) } } .padding(.horizontal, 12) .padding(.vertical, 8) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) .controlGroup(Capsule(style: .continuous), level: .primary) .padding(.top, 5) // Stats header HStack { Text("\(selectedFileItemsLocal.count)/\(filteredFiles.count) file\(filteredFiles.count == 1 ? "" : "s")") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() if let lastRefresh = lastRefreshDate { TimelineView(.periodic(from: lastRefresh, by: 1.0)) { _ in Text("Updated \(formatRelativeTime(lastRefresh))") .font(.caption) .monospacedDigit() .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } } .padding(.vertical) // File list (simple or categorized based on viewMode) ScrollView { if viewMode == .simple { // Simple flat list view LazyVStack(spacing: 0) { ForEach(Array(filteredFiles.enumerated()), id: \.element) { index, path in VStack(spacing: 0) { FileDetailsItem( path: path, removeAssociation: removeSingleZombieAssociation, isSelected: binding(for: path) ) if index < filteredFiles.count - 1 { Divider() } } } } .onAppear { updateSortedFiles() updateMemoizedFiles() } .onChange(of: sortedFiles) { _ in updateMemoizedFiles() } } else { // Categorized view LazyVStack(spacing: 8) { ForEach(categorizedFiles, id: \.category) { group in FileCategoryView( group: group, onToggleExpand: { withAnimation(.easeInOut(duration: animationEnabled ? 0.2 : 0)) { toggleCategory(group.category) } }, onToggleSelection: { toggleCategorySelection(group) }, fileItemBinding: binding(for:), removeAssociation: removeSingleZombieAssociation ) if group.category != categorizedFiles.last?.category { Divider() .padding(.vertical, 4) } } } .onAppear { updateSortedFiles() updateMemoizedFiles() } .onChange(of: sortedFiles) { _ in updateMemoizedFiles() } } } .scrollIndicators(scrollIndicators ? .automatic : .never) .onAppear { if lastRefreshDate == nil { lastRefreshDate = Date() } } .onChange(of: appState.showProgress) { isShowing in // When scan completes (showProgress becomes false), update refresh date if !isShowing && appState.appInfo.fileSize.keys.count > 0 { lastRefreshDate = Date() } } if appState.trashError { HStack { Spacer() InfoButton( text: "A trash error has occurred, please open the debug window(⌘+D) to see what went wrong or try again", color: .orange, label: "View Error", warning: true, extraView: { Button("View Debug Window") { windowController.open( with: ConsoleView(), width: 600, height: 400) } } ) .onDisappear { appState.trashError = false } .padding(.bottom) } } } .opacity(infoSidebar ? 0.5 : 1) .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(.horizontal, 20) .safeAreaInset(edge: .bottom) { if !selectedFileItemsLocal.isEmpty { HStack { Spacer() HStack(spacing: 10) { Button(selectedFileItemsLocal.count == filteredFiles.count ? "Deselect All" : "Select All") { if selectedFileItemsLocal.count == filteredFiles.count { selectedFileItemsLocal.removeAll() } else { selectedFileItemsLocal = Set(filteredFiles) } appState.selectedItems = selectedFileItemsLocal } .buttonStyle(ControlGroupButtonStyle( foregroundColor: ThemeColors.shared(for: colorScheme).accent, shape: Capsule(style: .continuous), level: .primary, skipControlGroup: true )) Divider().frame(height: 10) Button { handleUninstallAction() } label: { Label { Text("Delete \(totalSelectedSize)") } icon: { Image(systemName: "trash") } } .buttonStyle(ControlGroupButtonStyle( foregroundColor: ThemeColors.shared(for: colorScheme).accent, shape: Capsule(style: .continuous), level: .primary, skipControlGroup: true )) } .controlGroup(Capsule(style: .continuous), level: .primary) Spacer() } .padding([.horizontal, .bottom]) } } } if !appState.externalPaths.isEmpty { ScrollView(.horizontal, showsIndicators: scrollIndicators) { HStack(spacing: 10) { ForEach(appState.externalPaths, id: \.self) { path in HStack(spacing: 5) { Button(path.deletingPathExtension().lastPathComponent) { let newApp = AppInfoFetcher.getAppInfo(atPath: path)! updateOnMain { appState.appInfo = newApp } showAppInFiles( appInfo: newApp, appState: appState, locations: locations) } .buttonStyle(.link) Button { removePath(path) } label: { Image(systemName: "minus.circle") } .buttonStyle(.plain) } .controlSize(.small) } } } .padding() } } } // Helper function to create binding for individual files private func binding(for file: URL) -> Binding { Binding( get: { selectedFileItemsLocal.contains(file) && filteredFiles.contains(file) }, set: { isSelected in if isSelected { if filteredFiles.contains(file) { selectedFileItemsLocal.insert(file) } } else { selectedFileItemsLocal.remove(file) } // Sync with appState appState.selectedItems = selectedFileItemsLocal } ) } // Helper function to toggle category expansion private func toggleCategory(_ category: FileCategory) { if collapsedCategories.contains(category) { collapsedCategories.remove(category) } else { collapsedCategories.insert(category) } } // Helper function to toggle category selection private func toggleCategorySelection(_ group: GroupedFiles) { if group.allSelected { // Deselect all files in this category for file in group.files { selectedFileItemsLocal.remove(file) } } else { // Select all files in this category (that are in filteredFiles) let filesToSelect = group.files.filter { filteredFiles.contains($0) } for file in filesToSelect { selectedFileItemsLocal.insert(file) } } // Sync with appState appState.selectedItems = selectedFileItemsLocal } // Helper function to update memoized files private func updateMemoizedFiles() { memoizedFiles = sortedFiles // Sync local selection with appState selectedFileItemsLocal = appState.selectedItems } } ================================================ FILE: Pearcleaner/Views/FilesView/FilesSidebarView.swift ================================================ // // SidebarView.swift // Pearcleaner // // Created by Alin Lupascu on 7/31/25. // import Foundation import SwiftUI import AlinFoundation struct SidebarView: View { @EnvironmentObject var appState: AppState @Environment(\.colorScheme) var colorScheme @Binding var infoSidebar: Bool let displaySizeTotal: String var body: some View { if infoSidebar { HStack { Spacer() VStack(spacing: 0) { AppDetailsHeaderView(displaySizeTotal: displaySizeTotal) Divider().padding(.vertical, 5) AppDetails() Spacer() ExtraOptions() } .padding() .frame(width: 250) .ifGlassSidebar() } .background(.black.opacity(0.00000000001)) .transition(.move(edge: .trailing)) .onTapGesture { infoSidebar = false } } } } struct AppDetailsHeaderView: View { @EnvironmentObject var appState: AppState @Environment(\.colorScheme) var colorScheme let displaySizeTotal: String var body: some View { VStack(alignment: .leading, spacing: 8) { headerMain() if let buildNumber = appState.appInfo.appBuildNumber { headerDetailRow(label: "Version", value: "\(appState.appInfo.appVersion) (\(buildNumber))") } else { headerDetailRow(label: "Version", value: appState.appInfo.appVersion) } headerDetailRow(label: "Bundle", value: appState.appInfo.bundleIdentifier) headerDetailRow(label: "Total size of all files", value: displaySizeTotal) //MARK: Badges HStack(alignment: .center, spacing: 5) { if appState.appInfo.webApp { badge("web") } if appState.appInfo.wrapped { badge("iOS") } if appState.appInfo.arch != .empty { badge(appState.appInfo.arch.type) } badge(appState.appInfo.system ? "system" : "user") if appState.appInfo.brew { badge("brew") } if appState.appInfo.hasSparkle { badge("sparkle") } if appState.appInfo.isAppStore { badge("mas") } if appState.appInfo.steam { badge("steam") } } .padding(.bottom, 8) } .frame(maxWidth: .infinity, alignment: .leading) } @ViewBuilder private func headerMain() -> some View { VStack(alignment: .center) { if let appIcon = appState.appInfo.appIcon { Image(nsImage: appIcon) .resizable() .scaledToFit() .frame(width: 50, height: 50) .shadow(color: appState.appInfo.averageColor ?? .black, radius: 6) } Text(appState.appInfo.appName) .font(.title3) .fontWeight(.bold) .lineLimit(2) .padding(4) .padding(.horizontal, 2) // .background { // RoundedRectangle(cornerRadius: 8) // .fill(appState.appInfo.averageColor ?? .clear) // } } .frame(maxWidth: .infinity, alignment: .center) .padding(.vertical, 5) } @ViewBuilder private func headerDetailRow(label: String, value: String) -> some View { VStack(alignment: .leading) { Text(label) .font(.subheadline) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text(value) } .padding(.bottom, 5) } @ViewBuilder private func badge(_ text: String) -> some View { Text(text) .font(.footnote) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .padding(.horizontal, 6) .padding(.vertical, 2) .background(ThemeColors.shared(for: colorScheme).primaryText.opacity(0.1)) .clipShape(Capsule()) } } struct AppDetails: View { @EnvironmentObject var appState: AppState @EnvironmentObject var locations: Locations @Environment(\.colorScheme) var colorScheme @AppStorage("settings.general.searchSensitivity") private var globalSensitivityLevel: SearchSensitivityLevel = .strict @State private var isSliderActive: Bool = false private var localSensitivity: SearchSensitivityLevel { get { appState.perAppSensitivity[appState.appInfo.path.path] ?? globalSensitivityLevel } nonmutating set { appState.perAppSensitivity[appState.appInfo.path.path] = newValue } } var body: some View { VStack(alignment: .leading, spacing: 8) { detailRow(label: "Location", value: appState.appInfo.path.deletingLastPathComponent().path, location: true) detailRow(label: "Date Created", value: appState.appInfo.creationDate.map { formattedMDDate(from: $0) }) detailRow(label: "Date Added", value: appState.appInfo.dateAdded.map { formattedMDDate(from: $0) }) detailRow(label: "Modified Date", value: appState.appInfo.contentChangeDate.map { formattedMDDate(from: $0) }) detailRow(label: "Last Used Date".localized(), value: appState.appInfo.lastUsedDate.map { formattedMDDate(from: $0) }) Divider().padding(.vertical, 5) // Sensitivity Level Slider VStack(alignment: .leading, spacing: 8) { HStack { Text("Custom Sensitivity") .font(.subheadline) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() Text(localSensitivity.title) .font(.caption) .fontWeight(.semibold) .foregroundStyle(localSensitivity.color) .padding(.horizontal, 6) .padding(.vertical, 2) .background { RoundedRectangle(cornerRadius: 4) .fill(ThemeColors.shared(for: colorScheme).secondaryBG) } } HStack { Text("Fewer files").textCase(.uppercase).font(.caption2).foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Slider(value: Binding( get: { Double(localSensitivity.rawValue) }, set: { newValue in let newLevel = SearchSensitivityLevel(rawValue: Int(newValue)) ?? .strict localSensitivity = newLevel } ), in: 0...Double(SearchSensitivityLevel.allCases.count - 1), step: 1, onEditingChanged: { editing in isSliderActive = editing if !editing { // User finished adjusting the slider, now refresh with new sensitivity refreshFiles() } }) .tint(localSensitivity.color) Text("Most files").textCase(.uppercase).font(.caption2).foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } .padding(.bottom, 8) } .frame(maxWidth: .infinity, alignment: .leading) .padding(.vertical, 5) } private func refreshFiles() { // Refresh the file search with the current sensitivity level showAppInFiles(appInfo: appState.appInfo, appState: appState, locations: locations) } @ViewBuilder private func detailRow(label: String, value: String?, location: Bool = false) -> some View { VStack(alignment: .leading) { HStack(spacing: 2) { Text(label.localized()) if location { Button { NSWorkspace.shared.selectFile(appState.appInfo.path.path, inFileViewerRootedAtPath: appState.appInfo.path.deletingLastPathComponent().path) } label: { Image(systemName: "folder") } .buttonStyle(.borderless) } } .font(.subheadline) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text(value ?? "--") } .padding(.bottom, 5) } } struct ExtraOptions: View { @EnvironmentObject var appState: AppState @EnvironmentObject var brewManager: HomebrewManager @Environment(\.colorScheme) var colorScheme @AppStorage("settings.files.showSidebarOnLoad") private var showSidebarOnLoad: Bool = false // Translation selection sheet state @State private var languageSheetWindow: NSWindow? @State private var availableLanguages: [LanguageInfo] = [] @State private var selectedLanguagesToRemove: Set = [] @State private var isLoadingLanguages: Bool = false // Homebrew adoption state @State private var showAdoptionSheet: Bool = false @State private var isLoadingCasks: Bool = false var body: some View { HStack() { Text("Click to dismiss").font(.caption).foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText.opacity(0.5)) Spacer() Menu { Toggle("Show sidebar on view load", isOn: $showSidebarOnLoad) // Homebrew Adoption (only show for non-App Store and non-Homebrew apps) if !appState.appInfo.isAppStore && appState.appInfo.cask == nil { Divider() Button(isLoadingCasks ? "Loading..." : "Adopt with Homebrew") { // Lazy load casks only when user clicks if brewManager.allAvailableCasks.isEmpty { isLoadingCasks = true Task { await brewManager.loadAvailablePackages(appState: appState) await MainActor.run { isLoadingCasks = false showAdoptionSheet = true } } } else { showAdoptionSheet = true } } .disabled(isLoadingCasks) } Divider() if appState.appInfo.arch == .universal { Button("Lipo Architectures") { let title = NSLocalizedString("App Lipo", comment: "Lipo alert title") let message = String(format: NSLocalizedString("Pearcleaner will strip the %@ architecture from %@'s executable file to save space. Would you like to proceed?", comment: "Lipo alert message"), isOSArm() ? "intel" : "arm64", appState.appInfo.appName) showCustomAlert(title: title, message: message, style: .informational, onOk: { Task { // Kill app if running before lipo'ing to prevent corruption await killApp(appId: appState.appInfo.bundleIdentifier) let _ = thinAppBundleArchitecture(at: appState.appInfo.path, of: appState.appInfo.arch) } }) } } Menu("Translations") { Button("Auto Prune (Keep macOS Language)") { let title = NSLocalizedString("Prune Translations", comment: "Prune alert title") let message = String(format: NSLocalizedString("This will remove all unused language translation files except your macOS language", comment: "Prune alert message")) showCustomAlert(title: title, message: message, style: .warning, onOk: { Task { do { try await pruneLanguages(in: appState.appInfo.path.path, showAlert: true) } catch { printOS("Translation prune error: \(error)") } } }) } Button("Choose Languages...") { Task { await showLanguageSelectionSheet() } } } } label: { Label("Options", systemImage: "ellipsis.circle") } .menuStyle(.borderedButton) .menuIndicator(.hidden) .fixedSize() } .sheet(isPresented: $showAdoptionSheet) { AdoptionSheetView( appInfo: appState.appInfo, context: .filesView, isPresented: $showAdoptionSheet ) .environmentObject(brewManager) } } // MARK: - Language Selection Sheet private func showLanguageSelectionSheet() async { guard let parentWindow = NSApp.keyWindow ?? NSApp.windows.first(where: { $0.isVisible }) else { return } await MainActor.run { // Set initial loading state self.isLoadingLanguages = true self.availableLanguages = [] self.selectedLanguagesToRemove = [] // Create the SwiftUI view with loading state let contentView = TranslationSelectionSheet( appName: appState.appInfo.appName, appPath: appState.appInfo.path.path, languages: $availableLanguages, selectedLanguages: $selectedLanguagesToRemove, isLoading: $isLoadingLanguages, onConfirm: { if let sheetWindow = self.languageSheetWindow { parentWindow.endSheet(sheetWindow) } self.languageSheetWindow = nil Task { await performManualPrune() } }, onCancel: { if let sheetWindow = self.languageSheetWindow { parentWindow.endSheet(sheetWindow) } self.languageSheetWindow = nil } ) // Create sheet window let hostingController = NSHostingController(rootView: contentView) let sheetWindow = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 600, height: 500), styleMask: [.titled, .closable], backing: .buffered, defer: false ) sheetWindow.title = "Choose Translations" sheetWindow.contentViewController = hostingController sheetWindow.isReleasedWhenClosed = false // Present as sheet (shows loading state immediately) parentWindow.beginSheet(sheetWindow) self.languageSheetWindow = sheetWindow } // Load languages in background let languages = await findAvailableLanguages(in: appState.appInfo.path.path) // Update with results await MainActor.run { self.availableLanguages = languages self.isLoadingLanguages = false } } private func performManualPrune() async { // Filter selected languages from available languages by code let languagesToRemove = availableLanguages.filter { language in selectedLanguagesToRemove.contains(language.code) } do { try await pruneLanguagesManual(languagesToRemove: languagesToRemove) // Show success message await MainActor.run { let removedCount = languagesToRemove.count let keptCount = availableLanguages.count - removedCount showCustomAlert( title: "Translations Pruned", message: "Successfully removed \(removedCount) language\(removedCount == 1 ? "" : "s"). Kept \(keptCount) language\(keptCount == 1 ? "" : "s").", style: .informational ) } } catch { await MainActor.run { showCustomAlert( title: "Prune Failed", message: "Failed to prune translations: \(error.localizedDescription)", style: .critical ) } printOS("Manual translation prune error: \(error)") } } } ================================================ FILE: Pearcleaner/Views/FilesView/FilesView.swift ================================================ // // FilesView.swift // Pearcleaner // // Created by Alin Lupascu on 11/1/23. // import AlinFoundation import Foundation import SwiftUI struct FilesView: View { @EnvironmentObject var appState: AppState @EnvironmentObject var fsm: FolderSettingsManager @EnvironmentObject var locations: Locations @State private var showPop: Bool = false @State private var windowController = WindowManager() @AppStorage("settings.sentinel.enable") private var sentinel: Bool = false @AppStorage("settings.general.brew") private var brew: Bool = false // @AppStorage("settings.general.selectedSort") var selectedSortAlpha: Bool = true @AppStorage("settings.general.selectedSort") var selectedSort: SortOptionList = .name @AppStorage("settings.interface.fileListViewMode") private var viewMode: FileListViewMode = .simple @AppStorage("settings.general.filesWarning") private var warning: Bool = false @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true @AppStorage("settings.general.confirmAlert") private var confirmAlert: Bool = false @AppStorage("settings.interface.details") private var detailsEnabled: Bool = true @AppStorage("settings.general.oneshot") private var oneShotMode: Bool = false @AppStorage("settings.interface.scrollIndicators") private var scrollIndicators: Bool = false @AppStorage("settings.files.showSidebarOnLoad") private var showSidebarOnLoad: Bool = false @State private var showAlert = false @Environment(\.colorScheme) var colorScheme @State private var sortedFiles: [URL] = [] @State private var infoSidebar: Bool = false @AppStorage("settings.general.searchSensitivity") private var globalSensitivityLevel: SearchSensitivityLevel = .strict @ObservedObject private var consoleManager = GlobalConsoleManager.shared @State private var permissionsSheetWindow: NSWindow? var body: some View { var totalSelectedSize: String { var total: Int64 = 0 for url in appState.selectedItems { let size = appState.appInfo.fileSize[url] ?? 0 total += size } return formatByte(size: total).human } let displaySizeTotal = formatByte(size: appState.appInfo.totalSize).human VStack(alignment: .center) { if appState.showProgress { VStack { Spacer() ProgressView() // ProgressStepView(currentStep: appState.progressStep) Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) .ignoresSafeArea(.all) // .transition(.opacity) // .animation(.none, value: appState.showProgress) } else { ZStack { FileListView( sortedFiles: $sortedFiles, infoSidebar: $infoSidebar, selectedSort: $selectedSort, viewMode: $viewMode, locations: locations, windowController: windowController, handleUninstallAction: handleUninstallAction, displaySizeTotal: displaySizeTotal, totalSelectedSize: totalSelectedSize, updateSortedFiles: updateSortedFiles, removeSingleZombieAssociation: removeSingleZombieAssociation, removePath: removePath ) SidebarView(infoSidebar: $infoSidebar, displaySizeTotal: displaySizeTotal) .padding([.trailing, .bottom], 20) } .animation( animationEnabled ? .spring(response: 0.35, dampingFraction: 0.8) : .none, value: infoSidebar) } } .sheet( isPresented: $showAlert, content: { VStack(spacing: 10) { Text("Important") .font(.headline) Divider() Spacer() Text( "Always confirm the files marked for removal. In rare cases, unrelated files may be found when app names are too similar." ) .font(.subheadline) Spacer() Button("Close") { warning = true showAlert = false } .buttonStyle( SimpleButtonStyle( icon: "x.circle.fill", label: String(localized: "Close"), help: String(localized: "Dismiss"))) // Spacer() } .padding(15) .frame(width: 400, height: 250) .background(GlassEffect(material: .hudWindow, blendingMode: .behindWindow)) } ) .onAppear { if !warning { showAlert = true } if showSidebarOnLoad { infoSidebar = true } } .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("FileSearchViewShouldRefresh"))) { _ in // Refresh current app's files if !appState.appInfo.bundleIdentifier.isEmpty { let currentAppInfo = appState.appInfo showAppInFiles(appInfo: currentAppInfo, appState: appState, locations: locations) } } .toolbarBackground(.hidden, for: .windowToolbar) .toolbar { ToolbarItem { Spacer() } TahoeToolbarItem(isGroup: true) { Button { withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) { consoleManager.showConsole.toggle() } } label: { Label("Console", systemImage: consoleManager.showConsole ? "terminal.fill" : "terminal") } .help("Toggle console output") Button { showPermissionsSheet() } label: { Label("Permissions", systemImage: "lock.shield") } .help("View TCC permissions for this app") Button { viewMode = viewMode == .simple ? .categorized : .simple } label: { Label("View", systemImage: viewMode == .simple ? "list.bullet" : "checklist") } .help(viewMode == .simple ? "Switch to categorized view" : "Switch to simple view") Button { // Cycle through sort options let allOptions = SortOptionList.allCases if let currentIndex = allOptions.firstIndex(of: selectedSort) { let nextIndex = (currentIndex + 1) % allOptions.count selectedSort = allOptions[nextIndex] updateSortedFiles() } } label: { Label(selectedSort.title, systemImage: selectedSort.systemImage) } .help("Sort by \(selectedSort.title). Click to cycle through options") Button { GlobalConsoleManager.shared.appendOutput("Refreshing files for \(appState.appInfo.appName)...\n", source: CurrentPage.applications.title) let currentAppInfo = appState.appInfo updateOnMain { appState.selectedItems = [] } withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { showAppInFiles( appInfo: currentAppInfo, appState: appState, locations: locations) } GlobalConsoleManager.shared.appendOutput("✓ Refreshed files\n", source: CurrentPage.applications.title) } label: { Label("Refresh", systemImage: "arrow.counterclockwise") } Button { if GlobalConsoleManager.shared.showConsole { GlobalConsoleManager.shared.showConsole.toggle() } infoSidebar.toggle() } label: { Label("Info", systemImage: "sidebar.trailing") } .help("See app details") } } } // Function to handle the uninstall action private func handleUninstallAction() { showCustomAlert( enabled: confirmAlert, title: String(localized: "Warning"), message: String(localized: "Are you sure you want to remove these files?"), style: .warning, onOk: { Task { GlobalConsoleManager.shared.appendOutput("Starting deletion for \(appState.appInfo.appName)...\n", source: CurrentPage.applications.title) let selectedItemsArray = Array(appState.selectedItems) var appWasRemoved = false // Stop Sentinel FileWatcher momentarily to ignore .app bundle being sent to Trash sendStopNotificationFW() // Kill the app before proceeding await killApp(appId: appState.appInfo.bundleIdentifier) // Trash the files let _ = moveFilesToTrash(appState: appState, at: selectedItemsArray) // Always cleanup UI, regardless of whether files physically existed updateOnMain { // Remove selected items from app's file list appState.appInfo.fileSize = appState.appInfo.fileSize.filter { !selectedItemsArray.contains($0.key) } appState.appInfo.fileIcon = appState.appInfo.fileIcon.filter { !selectedItemsArray.contains($0.key) } appState.selectedItems.removeAll() updateSortedFiles() } // Determine if it's a full delete let appPath = appState.appInfo.path.absoluteString let appRemoved = selectedItemsArray.contains(where: { $0.absoluteString == appPath }) // For wrapped apps, also check if the container is being deleted let containerRemoved: Bool = { if appState.appInfo.wrapped { // Get container path by going up two levels from inner app // e.g., Container.app/Wrapper/ActualApp.app -> Container.app let containerPath = appState.appInfo.path .deletingLastPathComponent() // Remove ActualApp.app -> Container.app/Wrapper .deletingLastPathComponent() // Remove Wrapper -> Container.app return selectedItemsArray.contains(where: { $0.absoluteString == containerPath.absoluteString }) } return false }() let mainAppRemoved = !appState.appInfo.wrapped && appRemoved let wrappedAppRemoved = appState.appInfo.wrapped && (appRemoved || containerRemoved) let isInTrash = appState.appInfo.path.path.contains(".Trash") var deleteType: DeleteType if mainAppRemoved || wrappedAppRemoved || isInTrash { deleteType = .fullDelete } else { deleteType = .semiDelete } switch deleteType { case .fullDelete: // The main app bundle is deleted or is already in Trash (Sentinel delete) appWasRemoved = true // Remove the app from the app list await removeApp(appState: appState, withPath: appState.appInfo.path) GlobalConsoleManager.shared.appendOutput("✓ Completed full deletion for \(appState.appInfo.appName)\n", source: CurrentPage.applications.title) case .semiDelete: // Some files deleted but main app bundle remains // App remains in the list; removes only deleted items GlobalConsoleManager.shared.appendOutput("✓ Completed partial deletion for \(appState.appInfo.appName)\n", source: CurrentPage.applications.title) break } // Process the next app if in external mode processNextExternalApp( appWasRemoved: appWasRemoved, isInTrash: isInTrash) // Send Sentinel FileWatcher start notification sendStartNotificationFW() } }) } // Helper function to process the next external app private func processNextExternalApp(appWasRemoved: Bool, isInTrash: Bool) { // Store the current app path before any removal let currentAppPath = appState.appInfo.path // Check if the current app requires brew cleanup (Is brew cleanup enabled, was main app bundle removed or was main bundle in Trash) if brew && (appWasRemoved || isInTrash), let caskName = appState.appInfo.cask { // Set flag to show progress indicator updateOnMain { appState.isBrewCleanupInProgress = true } // Run Homebrew cleanup and WAIT for it to complete Task { do { try await HomebrewUninstaller.shared.uninstallPackage( name: caskName, cask: true, zap: true ) } catch { printOS("Homebrew cleanup failed for \(caskName): \(error.localizedDescription)") } // Remove the CURRENT app from the queue (not necessarily the first) await MainActor.run { if let index = appState.externalPaths.firstIndex(of: currentAppPath) { appState.externalPaths.remove(at: index) } } // Process remaining apps (transitions UI state) await processRemainingApps(appWasRemoved: appWasRemoved) // Clear progress flag AFTER UI has transitioned await MainActor.run { appState.isBrewCleanupInProgress = false } } return } // Remove the CURRENT app from the queue (not necessarily the first) if let index = appState.externalPaths.firstIndex(of: currentAppPath) { appState.externalPaths.remove(at: index) } // Continue processing remaining apps Task { await processRemainingApps(appWasRemoved: appWasRemoved) } } // Helper function to process remaining apps after current app is done private func processRemainingApps(appWasRemoved: Bool) async { // Check if there are more paths to process if !appState.externalPaths.isEmpty { // Get the next path if let nextPath = appState.externalPaths.first { // Load the next app's info if let nextApp = AppInfoFetcher.getAppInfo(atPath: nextPath) { updateOnMain { appState.appInfo = nextApp } showAppInFiles(appInfo: nextApp, appState: appState, locations: locations) } } } else { // All external apps processed if appWasRemoved { updateOnMain { appState.appInfo = .empty withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { appState.currentView = .empty } } } // Handle oneshot mode termination (only when launched externally for a single app) if oneShotMode && appState.externalMode && !appState.multiMode { updateOnMain { NSApp.terminate(nil) } } } } // Function to remove a path from externalPaths and update appInfo if necessary private func removePath(_ path: URL) { if let index = appState.externalPaths.firstIndex(of: path) { appState.externalPaths.remove(at: index) // Check if the removed path matches the current appInfo if appState.appInfo.path == path { // If there are more items in externalPaths, set appInfo to the next app if let nextPath = appState.externalPaths.first { let nextApp = AppInfoFetcher.getAppInfo(atPath: nextPath)! updateOnMain { appState.appInfo = nextApp } showAppInFiles(appInfo: nextApp, appState: appState, locations: locations) } else { // If no more items are left, set appInfo to .empty and change page to default view updateOnMain { appState.appInfo = .empty withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { appState.currentView = .empty } } } } } } private func updateSortedFiles() { let sortedFilesSize = appState.appInfo.fileSize.keys.sorted(by: { appState.appInfo.fileSize[$0, default: 0] > appState.appInfo.fileSize[$1, default: 0] }) let sortedFilesAlpha = appState.appInfo.fileSize.keys.sorted { firstURL, secondURL in let isFirstPathApp = firstURL.pathExtension == "app" let isSecondPathApp = secondURL.pathExtension == "app" if isFirstPathApp, !isSecondPathApp { return true } else if !isFirstPathApp, isSecondPathApp { return false } else { // return firstURL.lastPathComponent.pearFormat() < secondURL.lastPathComponent.pearFormat() return showLocalized(url: firstURL).localizedCaseInsensitiveCompare( showLocalized(url: secondURL)) == .orderedAscending } } let sortedFilesByPath = appState.appInfo.fileSize.keys.sorted { firstURL, secondURL in let isFirstPathApp = firstURL.pathExtension == "app" let isSecondPathApp = secondURL.pathExtension == "app" if isFirstPathApp, !isSecondPathApp { return true } else if !isFirstPathApp, isSecondPathApp { return false } else { return firstURL.path.localizedCaseInsensitiveCompare(secondURL.path) == .orderedAscending } } switch selectedSort { case .size: sortedFiles = sortedFilesSize case .name: sortedFiles = sortedFilesAlpha case .path: sortedFiles = sortedFilesByPath } // sortedFiles = selectedSortAlpha ? sortedFilesAlpha : sortedFilesSize } private func removeZombieAssociations() { updateOnMain { let associatedFiles = ZombieFileStorage.shared.getAssociatedFiles( for: appState.appInfo.path) // Remove associated files from appInfo storage appState.appInfo.fileSize = appState.appInfo.fileSize.filter { !associatedFiles.contains($0.key) } appState.appInfo.fileIcon = appState.appInfo.fileIcon.filter { !associatedFiles.contains($0.key) } // Remove from sorted list sortedFiles.removeAll { associatedFiles.contains($0) } // Remove from selected items appState.selectedItems = appState.selectedItems.filter { !associatedFiles.contains($0) } // Clear stored associations ZombieFileStorage.shared.clearAssociations(for: appState.appInfo.path) updateSortedFiles() } } private func removeSingleZombieAssociation(_ path: URL) { updateOnMain { var associatedFiles = ZombieFileStorage.shared.getAssociatedFiles( for: appState.appInfo.path) // Remove only the specified path associatedFiles.removeAll { $0 == path } // Update app info storage appState.appInfo.fileSize.removeValue(forKey: path) appState.appInfo.fileIcon.removeValue(forKey: path) // Update sorted list sortedFiles.removeAll { $0 == path } // Update selected items appState.selectedItems.remove(path) // Update stored associations if associatedFiles.isEmpty { ZombieFileStorage.shared.clearAssociations(for: appState.appInfo.path) } else { ZombieFileStorage.shared.associatedFiles[appState.appInfo.path] = associatedFiles } // Remove from exclusion list fsm.removePathZ(path.path) // Final update updateSortedFiles() } } // MARK: - Show Permissions Sheet private func showPermissionsSheet() { guard let parentWindow = NSApp.keyWindow ?? NSApp.windows.first(where: { $0.isVisible }) else { return } let contentView = TCCPermissionViewer( bundleIdentifier: appState.appInfo.bundleIdentifier, appName: appState.appInfo.appName, onClose: { if let sheetWindow = self.permissionsSheetWindow { parentWindow.endSheet(sheetWindow) } self.permissionsSheetWindow = nil } ) let hostingController = NSHostingController(rootView: contentView) let sheetWindow = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 700, height: 500), styleMask: [.titled, .closable], backing: .buffered, defer: false ) sheetWindow.title = "TCC Permissions" sheetWindow.contentViewController = hostingController sheetWindow.isReleasedWhenClosed = false parentWindow.beginSheet(sheetWindow) self.permissionsSheetWindow = sheetWindow } } // Define the DeleteType enum enum DeleteType { case fullDelete case semiDelete } struct FileDetailsItem: View { @EnvironmentObject var appState: AppState @Environment(\.colorScheme) var colorScheme @State private var isHovered = false @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true let path: URL let removeAssociation: (URL) -> Void @Binding var isSelected: Bool var body: some View { let size = appState.appInfo.fileSize[path] ?? 0 let fileIcon = appState.appInfo.fileIcon[path] let iconImage = fileIcon.flatMap { $0.map(Image.init(nsImage:)) } let displaySize = formatByte(size: size).human HStack(alignment: .center, spacing: 15) { Button(action: { if !self.path.path.contains(".Trash") { isSelected.toggle() } }) { EmptyView() } .buttonStyle(CircleCheckboxButtonStyle(isSelected: isSelected)) .disabled(self.path.path.contains(".Trash")) if let appIcon = iconImage { appIcon .resizable() .aspectRatio(contentMode: .fit) .frame(width: 30, height: 30) .clipShape(RoundedRectangle(cornerRadius: 8)) } VStack(alignment: .leading, spacing: 5) { HStack(alignment: .center) { Text(showLocalized(url: path)) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .font(.title3) .lineLimit(1) .truncationMode(.tail) .help(path.lastPathComponent) .overlay { if isHovered { VStack { Spacer() RoundedRectangle(cornerRadius: 10) .fill( ThemeColors.shared(for: colorScheme).primaryText .opacity(0.5) ) .frame(height: 1.5) .offset(y: 3) } } } if isNested(path: path) { InfoButton( text: String( localized: "Application file is nested within subdirectories. To prevent deleting incorrect folders, Pearcleaner will leave these alone. You may manually delete the remaining folders if required." )) } if let imageView = folderImages(for: path.path) { imageView } if ZombieFileStorage.shared.isPathAssociated(path) { Image(systemName: "link") .resizable() .scaledToFit() .frame(width: 13) .foregroundStyle( ThemeColors.shared(for: colorScheme).primaryText.opacity(0.5)) } } path.path.pathWithArrows( separatorColor: ThemeColors.shared(for: colorScheme).primaryText ) .font(.footnote) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .help(path.path) } .onTapGesture { NSWorkspace.shared.selectFile( path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path) } .onHover { hovering in withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { self.isHovered = hovering } } Spacer() Text(verbatim: "\(displaySize)") .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } .padding(.vertical, 8) .contextMenu { if path.pathExtension == "app" { Button("Open \(path.deletingPathExtension().lastPathComponent)") { NSWorkspace.shared.open(path) } Divider() } Button("Copy Path") { copyToClipboard(text: path.path) } Button("View in Finder") { NSWorkspace.shared.selectFile( path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path) } if ZombieFileStorage.shared.isPathAssociated(path) { Button("Unlink File") { removeAssociation(path) } } } } } ================================================ FILE: Pearcleaner/Views/FilesView/TranslationSelectionSheet.swift ================================================ // // TranslationSelectionSheet.swift // Pearcleaner // // Created by Claude on 10/20/25. // import SwiftUI import AlinFoundation struct TranslationSelectionSheet: View { let appName: String let appPath: String @Binding var languages: [LanguageInfo] @Binding var selectedLanguages: Set @Binding var isLoading: Bool let onConfirm: () -> Void let onCancel: () -> Void @State private var searchText = "" @Environment(\.colorScheme) var colorScheme private var filteredLanguages: [LanguageInfo] { if searchText.isEmpty { return languages } return languages.filter { language in language.displayName.localizedCaseInsensitiveContains(searchText) || language.code.localizedCaseInsensitiveContains(searchText) } } private var allSelected: Bool { selectedLanguages.count == languages.count } var body: some View { StandardSheetView( title: "Choose Translations to Remove", width: 600, height: 500, onClose: onCancel ) { // Content VStack(spacing: 0) { // Subtitle with app name VStack(spacing: 8) { Text(appName) .font(.headline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) if isLoading { Text("Loading languages...") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } else if languages.isEmpty { Text("No translations found") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } else { Text("\(selectedLanguages.count) of \(languages.count) language\(languages.count == 1 ? "" : "s") selected for removal") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } // Search bar (only show if not loading and has languages) if !isLoading && !languages.isEmpty { HStack { Image(systemName: "magnifyingglass") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) TextField("Filter languages...", text: $searchText) .textFieldStyle(.plain) if !searchText.isEmpty { Button { searchText = "" } label: { Image(systemName: "xmark.circle.fill") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .buttonStyle(.plain) } } .padding(.top, 12) Divider() .padding(.top, 12) } // Language list, loading state, or empty state if isLoading { // Loading state VStack(spacing: 12) { ProgressView() .scaleEffect(1.5) Text("Finding available languages...") .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .frame(maxWidth: .infinity, maxHeight: 400) .frame(minHeight: 200) } else if languages.isEmpty { // Empty state - no languages found VStack(spacing: 12) { Image(systemName: "exclamationmark.triangle") .font(.system(size: 48)) .foregroundStyle(.orange) Text("No translations found") .font(.title3) .fontWeight(.semibold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Text("This app has no removable language translation files.") .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .multilineTextAlignment(.center) .padding(.horizontal, 40) } .frame(maxWidth: .infinity, maxHeight: 400) .frame(minHeight: 200) } else { ScrollView { LazyVStack(alignment: .leading, spacing: 2) { ForEach(filteredLanguages) { language in HStack(spacing: 8) { Button { toggleSelection(language.code) } label: { Image(systemName: selectedLanguages.contains(language.code) ? "checkmark.square.fill" : "square") .foregroundStyle(selectedLanguages.contains(language.code) ? .blue : ThemeColors.shared(for: colorScheme).secondaryText) } .buttonStyle(.plain) HStack(spacing: 6) { Text(language.displayName) .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Text(verbatim: "(\(language.code))") .font(.caption2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) if language.isPreferred { Image(systemName: "star.fill") .font(.caption2) .foregroundStyle(.orange) .help("Preferred language") } } Spacer() } .padding(.horizontal) .padding(.vertical, 6) .background(filteredLanguages.firstIndex(of: language).map { $0 % 2 == 0 } == true ? Color.clear : ThemeColors.shared(for: colorScheme).secondaryText.opacity(0.05)) } } .padding(.vertical, 8) } .frame(maxHeight: 400) } } } selectionControls: { // Selection controls (only show if not loading and has languages) if !isLoading && !languages.isEmpty { HStack(spacing: 12) { Button { selectAll() } label: { Text("Select All") .font(.caption) } .buttonStyle(.borderless) .disabled(allSelected) Button { deselectAll() } label: { Text("Deselect All") .font(.caption) } .buttonStyle(.borderless) .disabled(selectedLanguages.isEmpty) } } } actionButtons: { Button("Cancel") { onCancel() } .keyboardShortcut(.cancelAction) Button("Remove Selected") { onConfirm() } .keyboardShortcut(.defaultAction) .disabled(isLoading || selectedLanguages.isEmpty) } } private func toggleSelection(_ languageCode: String) { if selectedLanguages.contains(languageCode) { selectedLanguages.remove(languageCode) } else { selectedLanguages.insert(languageCode) } } private func selectAll() { selectedLanguages = Set(languages.map { $0.code }) } private func deselectAll() { selectedLanguages.removeAll() } } ================================================ FILE: Pearcleaner/Views/LipoView/LipoSidebarView.swift ================================================ // // LipoSidebarView.swift // Pearcleaner // // Created by Alin Lupascu on 8/9/25. // import AlinFoundation import SwiftUI // Break up the sidebar into smaller components struct LipoSidebarView: View { @EnvironmentObject var appState: AppState @Environment(\.colorScheme) var colorScheme @Binding var infoSidebar: Bool let excludedApps: Set @Binding var prune: Bool @Binding var filterMinSavings: Bool let onRemoveExcluded: (String) -> Void let totalSpaceSaved: UInt64 let savingsAllApps: UInt64 var body: some View { if infoSidebar { HStack { Spacer() VStack(alignment: .leading, spacing: 12) { LipoDescriptionSection() Divider() LipoSavingsSection( totalSpaceSaved: totalSpaceSaved, savingsAllApps: savingsAllApps) Divider() LipoExcludedAppsSection( excludedApps: excludedApps, onRemoveExcluded: onRemoveExcluded) Spacer() LipoOptionsSection(prune: $prune, filterMinSavings: $filterMinSavings) } .padding() .frame(width: 280) .ifGlassSidebar() .padding([.trailing, .bottom], 20) } .background(.black.opacity(0.00000000001)) .transition(.move(edge: .trailing)) .onTapGesture { infoSidebar = false } } } } // Description component struct LipoDescriptionSection: View { @State private var showFullDescription = false @Environment(\.colorScheme) var colorScheme private let shortDescription = "App lipo targets the Mach-O binaries inside your universal app bundles and removes any unused architectures..." private let fullDescription = "App lipo targets the Mach-O binaries inside your universal app bundles and removes any unused architectures, such as x86_64 or arm64, leaving only the architectures your computer actually supports. The list shows only universal type apps, not your full app list. After lipo, the green portion will be removed from your app's binary. It's recommended to open an app at least once before lipo to make sure macOS has cached the signature. Privileged Helper is required to perform this action on certain applications." var body: some View { VStack(alignment: .leading, spacing: 8) { Text(showFullDescription ? fullDescription : shortDescription) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .fixedSize(horizontal: false, vertical: true) Button(showFullDescription ? "Less" : "More") { withAnimation(.easeInOut(duration: 0.2)) { showFullDescription.toggle() } } .font(.caption2) .buttonStyle(.link) } } } struct LipoSavingsSection: View { @Environment(\.colorScheme) var colorScheme let totalSpaceSaved: UInt64 let savingsAllApps: UInt64 var body: some View { VStack(alignment: .leading) { HStack(spacing: 0) { Text("Total Saved:") .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Spacer() Text(formatByte(size: Int64(totalSpaceSaved)).human) .foregroundStyle(.green) } HStack(spacing: 0) { Text("Approximate Savings:") .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Spacer() Text(formatByte(size: Int64(savingsAllApps)).human) .foregroundStyle(.orange) } } } } // Excluded apps component struct LipoExcludedAppsSection: View { @EnvironmentObject var appState: AppState @Environment(\.colorScheme) var colorScheme let excludedApps: Set let onRemoveExcluded: (String) -> Void // Create a computed property that sorts the excluded apps alphabetically private var sortedExcludedApps: [String] { Array(excludedApps).sorted { appPath1, appPath2 in let app1 = appState.sortedApps.first(where: { $0.path.path == appPath1 }) let app2 = appState.sortedApps.first(where: { $0.path.path == appPath2 }) let name1 = app1?.appName ?? "" let name2 = app2?.appName ?? "" return name1.localizedCaseInsensitiveCompare(name2) == .orderedAscending } } var body: some View { VStack(alignment: .leading, spacing: 8) { Text("Excluded Apps") .font(.subheadline) .fontWeight(.medium) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) if excludedApps.isEmpty { Text("No apps excluded") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .italic() } else { ScrollView { LazyVStack(alignment: .leading, spacing: 4) { ForEach(sortedExcludedApps, id: \.self) { appPath in LipoExcludedAppRow(appPath: appPath, onRemoveExcluded: onRemoveExcluded) } } } .frame(maxHeight: 200) } } } } // Individual excluded app row component struct LipoExcludedAppRow: View { @EnvironmentObject var appState: AppState @Environment(\.colorScheme) var colorScheme let appPath: String let onRemoveExcluded: (String) -> Void var body: some View { if let appInfo = appState.sortedApps.first(where: { $0.path.path == appPath }) { HStack { if let appIcon = appInfo.appIcon { Image(nsImage: appIcon) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 16, height: 16) } VStack(alignment: .leading, spacing: 1) { Text(appInfo.appName) .font(.caption) .lineLimit(1) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Text(appInfo.appVersion) .font(.caption2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .lineLimit(1) } Spacer() Button { onRemoveExcluded(appPath) } label: { Image(systemName: "minus.circle") .foregroundStyle(.red) } .buttonStyle(.borderless) .help("Remove from exclusion list") } .padding(8) .background(ThemeColors.shared(for: colorScheme).secondaryText.opacity(0.1)) .cornerRadius(6) } } } // Prune toggle component struct LipoOptionsSection: View { @Binding var prune: Bool @Binding var filterMinSavings: Bool @Environment(\.colorScheme) var colorScheme var body: some View { HStack { Text("Click to dismiss").font(.caption).foregroundStyle( ThemeColors.shared(for: colorScheme).primaryText.opacity(0.5)) Spacer() Menu { Toggle( isOn: $prune, label: { Text("Remove unused languages during lipo") .font(.caption) }) Toggle( isOn: $filterMinSavings, label: { Text("Only show apps with savings of 1MB+") .font(.caption) }) } label: { Label("Options", systemImage: "ellipsis.circle") } .menuStyle(.borderedButton) .menuIndicator(.hidden) .fixedSize() } } } struct LipoLegend: View { @Environment(\.colorScheme) var colorScheme var body: some View { HStack { RoundedRectangle(cornerRadius: 4).fill(.green).frame(width: 12, height: 12) Text("Approximate Savings").foregroundStyle( ThemeColors.shared(for: colorScheme).secondaryText) } } } ================================================ FILE: Pearcleaner/Views/LipoView/LipoView.swift ================================================ // // LipoView.swift // Pearcleaner // // Created by Alin Lupascu on 3/20/25. // import AlinFoundation import SwiftUI struct LipoView: View { @EnvironmentObject var appState: AppState @Environment(\.colorScheme) var colorScheme @ObservedObject private var consoleManager = GlobalConsoleManager.shared @State private var selectedApps: Set = [] @State private var isProcessing: Bool = false @State private var savingsAllApps: UInt64 = 0 @State private var bundleAllApps: UInt64 = 0 @State private var sliceSizesByPath = [String: (bundle: UInt64, savings: UInt64)]() @State private var totalSpaceSaved: UInt64 = 0 @State private var infoSidebar: Bool = false @State private var selectedSort: LipoSortOption = .name @State private var searchText: String = "" @State private var lastRefreshDate: Date? @State private var isRefreshing: Bool = false @AppStorage("settings.interface.scrollIndicators") private var scrollIndicators: Bool = false @AppStorage("settings.lipo.pruneTranslations") private var prune = false @AppStorage("settings.lipo.filterMinSavings") private var filterMinSavings = false @AppStorage("settings.lipo.showZeroPercentSavings") private var showZeroPercentSavings: Bool = false @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true @AppStorage("settings.lipo.excludedApps") private var excludedAppsData: Data = Data() @AppStorage("settings.lipo.warning") private var warning: Bool = false @State private var showAlert = false enum LipoSortOption: String, CaseIterable { case name = "Name" case savings = "Savings Size" case binary = "Bundle Size" var systemImage: String { switch self { case .name: return "list.bullet" case .savings: return "arrow.down.circle" case .binary: return "doc.circle" } } } // Change to a computed property without setter private var excludedApps: Set { if let decoded = try? JSONDecoder().decode(Set.self, from: excludedAppsData) { return decoded } return Set() } // Add helper methods for excluded apps management private func addToExcluded(_ apps: Set) { var current = excludedApps current.formUnion(apps) if let encoded = try? JSONEncoder().encode(current) { excludedAppsData = encoded } } // Rename the helper method to avoid conflict private func removeAppFromExcluded(_ appPath: String) { var current = excludedApps current.remove(appPath) if let encoded = try? JSONEncoder().encode(current) { excludedAppsData = encoded } // Sizes will be recalculated per-app on-demand } // Filter and sort the apps (Note: "universalApps" name is legacy - now includes all apps) var universalApps: [AppInfo] { // Show all apps since our new bundle thinning can find savings even in apps // whose main executable isn't universal (frameworks, plugins, etc. might be) var filtered = appState.sortedApps.filter { !excludedApps.contains($0.path.path) } // Apply search filter if !searchText.isEmpty { filtered = filtered.filter { app in app.appName.localizedCaseInsensitiveContains(searchText) || app.path.path.localizedCaseInsensitiveContains(searchText) } } var result = filtered // Hide apps with 0% savings unless user chooses to show them if !showZeroPercentSavings && !sliceSizesByPath.isEmpty { result = result.filter { app in if let sizes = sliceSizesByPath[app.path.path] { // Calculate percentage savings like in the UI let percentSavings = app.bundleSize > 0 ? Int((Double(sizes.savings) / Double(app.bundleSize)) * 100) : 0 return percentSavings > 0 } return true // Show uncalculated apps } } if filterMinSavings { // Only apply the 1MB+ filter if we have size data calculated if !sliceSizesByPath.isEmpty { result = result.filter { app in if let sizes = sliceSizesByPath[app.path.path] { return sizes.savings >= 1024 * 1024 // 1MB in bytes } return false } } } // Apply sorting return result.sorted { app1, app2 in switch selectedSort { case .name: return app1.appName.localizedCaseInsensitiveCompare(app2.appName) == .orderedAscending case .savings: let savings1 = sliceSizesByPath[app1.path.path]?.savings ?? 0 let savings2 = sliceSizesByPath[app2.path.path]?.savings ?? 0 return savings1 > savings2 // Descending order for savings case .binary: let bundle1 = UInt64(app1.bundleSize) let bundle2 = UInt64(app2.bundleSize) return bundle1 > bundle2 // Descending order for bundle size } } } var body: some View { ZStack { VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) { // Add this section back - the main app list content // Search bar HStack { Image(systemName: "magnifyingglass") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) TextField("Search...", text: $searchText) .textFieldStyle(.plain) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) if !searchText.isEmpty { Button { searchText = "" } label: { Image(systemName: "xmark.circle.fill") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .buttonStyle(.plain) } } .padding(.horizontal, 12) .padding(.vertical, 8) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) .controlGroup(Capsule(style: .continuous), level: .primary) .padding(.top, 5) if universalApps.isEmpty { VStack { Spacer() Text("No apps available for thinning") .font(.title2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() } .frame(maxWidth: .infinity) } else { // Stats header HStack { Text("\(universalApps.count) app\(universalApps.count == 1 ? "" : "s")") .font(.caption) .monospacedDigit() .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) if isRefreshing { Text("Refreshing...") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } Spacer() if let lastRefresh = lastRefreshDate { TimelineView(.periodic(from: lastRefresh, by: 1.0)) { _ in Text("Updated \(formatRelativeTime(lastRefresh))") .font(.caption) .monospacedDigit() .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } LipoLegend() } .padding(.vertical) ScrollView { LazyVStack(spacing: 10) { ForEach(universalApps, id: \.path) { app in LipoAppRowView( app: app, selectedApps: $selectedApps, sliceSizesByPath: $sliceSizesByPath, savingsAllApps: $savingsAllApps, bundleAllApps: $bundleAllApps ) } } } .scrollIndicators(scrollIndicators ? .automatic : .never) } } } .opacity(infoSidebar ? 0.5 : 1) .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(.horizontal, 20) .safeAreaInset(edge: .bottom) { if !selectedApps.isEmpty { HStack { Spacer() HStack(spacing: 10) { Button(selectedApps.count == universalApps.count ? "Deselect All" : "Select All") { if selectedApps.count == universalApps.count { selectedApps.removeAll() } else { selectedApps = Set(universalApps.map { $0.path.path }) } } .buttonStyle(ControlGroupButtonStyle( foregroundColor: ThemeColors.shared(for: colorScheme).accent, shape: Capsule(style: .continuous), level: .primary, skipControlGroup: true )) Divider().frame(height: 10) Button { excludeSelectedApps() } label: { Label { Text("Exclude \(selectedApps.count) Selected") } icon: { Image(systemName: "minus.circle") } } .buttonStyle(ControlGroupButtonStyle( foregroundColor: ThemeColors.shared(for: colorScheme).accent, shape: Capsule(style: .continuous), level: .primary, skipControlGroup: true )) Divider().frame(height: 10) Button { startLipo() } label: { if !isProcessing { Label { Text("Start Lipo") } icon: { Image(systemName: "scissors") } } else { ProgressView() .controlSize(.mini) } } .buttonStyle(ControlGroupButtonStyle( foregroundColor: ThemeColors.shared(for: colorScheme).accent, shape: Capsule(style: .continuous), level: .primary, skipControlGroup: true )) } .controlGroup(Capsule(style: .continuous), level: .primary) Spacer() } .padding([.horizontal, .bottom]) } } // Add the sidebar view LipoSidebarView( infoSidebar: $infoSidebar, excludedApps: excludedApps, prune: $prune, filterMinSavings: $filterMinSavings, onRemoveExcluded: removeAppFromExcluded, totalSpaceSaved: totalSpaceSaved, savingsAllApps: savingsAllApps) } .animation( animationEnabled ? .spring(response: 0.35, dampingFraction: 0.8) : .none, value: infoSidebar ) .onAppear { if !warning { showAlert = true } if lastRefreshDate == nil { lastRefreshDate = Date() } // Sizes will be calculated per-app on-demand } .onDisappear { // No background tasks to cancel with per-app calculation } .sheet( isPresented: $showAlert, content: { VStack(spacing: 10) { Text("Important") .font(.headline) Divider() Spacer() Text( "Bundle thinning (lipo) is an aggressive operation that modifies the binaries within app bundles by removing unused architectures. While generally safe, some applications may experience issues or fail to launch after this process. It is strongly recommended to create a backup of your applications before proceeding, especially for critical or frequently used apps." ) .font(.subheadline) Spacer() Button("Close") { warning = true showAlert = false } .buttonStyle( SimpleButtonStyle( icon: "x.circle.fill", label: String(localized: "Close"), help: String(localized: "Dismiss"))) Spacer() } .padding(15) .frame(width: 400, height: 250) .background(GlassEffect(material: .hudWindow, blendingMode: .behindWindow)) }) .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("LipoViewShouldRefresh"))) { _ in refreshList() } .toolbarBackground(.hidden, for: .windowToolbar) .toolbar { TahoeToolbarItem(placement: .navigation) { VStack(alignment: .leading) { Text("Lipo").foregroundStyle( ThemeColors.shared(for: colorScheme).primaryText ).font(.title2).fontWeight(.bold) Text( "Remove unused architectures from your app binaries to reduce app size" ) .font(.callout).foregroundStyle( ThemeColors.shared(for: colorScheme).secondaryText) } } ToolbarItem { Spacer() } TahoeToolbarItem(isGroup: true) { Button { withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) { consoleManager.showConsole.toggle() } } label: { Label("Console", systemImage: consoleManager.showConsole ? "terminal.fill" : "terminal") } .help("Toggle console output") Menu { ForEach(LipoSortOption.allCases, id: \.self) { sortOption in Button { selectedSort = sortOption } label: { Label(sortOption.rawValue, systemImage: sortOption.systemImage) } } } label: { Label(selectedSort.rawValue, systemImage: selectedSort.systemImage) } .labelStyle(.titleAndIcon) .menuIndicator(.hidden) Button { refreshList() } label: { Label("Refresh", systemImage: "arrow.counterclockwise") } .disabled(isRefreshing) Button { if GlobalConsoleManager.shared.showConsole { GlobalConsoleManager.shared.showConsole.toggle() } infoSidebar.toggle() } label: { Label("Info", systemImage: "sidebar.trailing") } .help("See lipo details") } } } private func refreshList() { GlobalConsoleManager.shared.appendOutput("Refreshing app list...\n", source: CurrentPage.lipo.title) isRefreshing = true Task { // Clear the cached sizes to force recalculation await MainActor.run { sliceSizesByPath.removeAll() savingsAllApps = 0 bundleAllApps = 0 } // Wait a bit for the UI to update try? await Task.sleep(nanoseconds: 500_000_000) await MainActor.run { lastRefreshDate = Date() isRefreshing = false GlobalConsoleManager.shared.appendOutput("✓ Refreshed app list\n", source: CurrentPage.lipo.title) } } } private func excludeSelectedApps() { addToExcluded(selectedApps) selectedApps.removeAll() // Sizes will be recalculated per-app on-demand } private func startLipo() { GlobalConsoleManager.shared.appendOutput("Starting lipo operation on \(selectedApps.count) app(s)...\n", source: CurrentPage.lipo.title) isProcessing = true Task { var totalPreSize: UInt64 = 0 var totalPostSize: UInt64 = 0 for app in universalApps where selectedApps.contains(app.path.path) { // Kill app if running before lipo'ing to prevent corruption await killApp(appId: app.bundleIdentifier) // Use the updated thinAppBundleArchitecture function with multi=true let (success, sizes) = thinAppBundleArchitecture( at: app.path, of: app.arch, multi: true) if success, let sizes = sizes { totalPreSize += sizes["pre"] ?? 0 totalPostSize += sizes["post"] ?? 0 } // Prune languages if enabled if prune { do { try await pruneLanguages(in: app.path.path) } catch { printOS("Translation prune error: \(error)") } } } let overallSavings = totalPreSize > 0 ? Int((Double(totalPreSize - totalPostSize) / Double(totalPreSize)) * 100) : 0 let titleFormat = NSLocalizedString( "Space Savings: %d%%\nTotal Space Saved: %@", comment: "Lipo completion title") let messageFormat = NSLocalizedString( "The total space savings between all the lipo'd apps\nSize Before: %@\nSize After: %@", comment: "Lipo completion message") let actualSpaceSaved = totalPreSize - totalPostSize let title = String( format: titleFormat, overallSavings, formatByte(size: Int64(actualSpaceSaved)).human ) let message = String( format: messageFormat, formatByte(size: Int64(totalPreSize)).human, formatByte(size: Int64(totalPostSize)).human) await MainActor.run { self.totalSpaceSaved += actualSpaceSaved GlobalConsoleManager.shared.appendOutput("✓ Completed lipo operation - saved \(formatByte(size: Int64(actualSpaceSaved)).human)\n", source: CurrentPage.lipo.title) showCustomAlert(title: title, message: message, style: .informational) self.isProcessing = false // Recalculate savings for processed apps to update their display and filter them out self.recalculateProcessedApps() } } } private func recalculateProcessedApps() { // Get a copy of currently selected apps before clearing the selection let processedAppPaths = Array(selectedApps) // Clear selections since these apps were processed selectedApps.removeAll() // Recalculate savings for each processed app in the background Task { for appPath in processedAppPaths { // Find the app info if let app = appState.sortedApps.first(where: { $0.path.path == appPath }) { let savings = await calculateBundleSavings(for: app) await MainActor.run { // Update the shared state with new (likely 0) savings sliceSizesByPath[appPath] = ( bundle: UInt64(app.bundleSize), savings: savings ) // Update totals - subtract old savings and add new (likely 0) if savings == 0 { // App was successfully processed and now has 0 savings - it will be filtered out savingsAllApps = sliceSizesByPath.values.reduce(0) { $0 + $1.savings } bundleAllApps = sliceSizesByPath.values.reduce(0) { $0 + $1.bundle } } } } } } } private func calculateBundleSavings(for app: AppInfo) async -> UInt64 { return await withCheckedContinuation { continuation in let appPath = app.path let appArch = app.arch DispatchQueue.global(qos: .utility).async { let (success, sizes) = thinAppBundleArchitecture( at: appPath, of: appArch, multi: true, dryRun: true) if success, let sizes = sizes { let preSize = sizes["pre"] ?? 0 let postSize = sizes["post"] ?? 0 let savings = preSize > postSize ? preSize - postSize : 0 continuation.resume(returning: savings) } else { continuation.resume(returning: 0) } } } } // Old bulk calculation function removed - now using per-app calculation } // New per-app row view that calculates bundle savings on-demand struct LipoAppRowView: View { @Environment(\.colorScheme) var colorScheme let app: AppInfo @Binding var selectedApps: Set @Binding var sliceSizesByPath: [String: (bundle: UInt64, savings: UInt64)] @Binding var savingsAllApps: UInt64 @Binding var bundleAllApps: UInt64 @State private var isCalculating: Bool = false @State private var calculatedSavings: UInt64? var body: some View { HStack(spacing: 15) { appToggle appContentView } .onAppear { // Priority 1: Check AppInfo cache (from SwiftData background pre-calculation) if let cachedSavings = app.lipoSavings { calculatedSavings = UInt64(cachedSavings) sliceSizesByPath[app.path.path] = (bundle: UInt64(app.bundleSize), savings: UInt64(cachedSavings)) } // Priority 2: Check local session cache else if let existingSizes = sliceSizesByPath[app.path.path] { calculatedSavings = existingSizes.savings } // Priority 3: Calculate on-demand (fallback for apps not yet pre-calculated) else if !isCalculating { calculateBundleSavings() } } } private var appToggle: some View { Button(action: { if selectedApps.contains(app.path.path) { selectedApps.remove(app.path.path) } else { selectedApps.insert(app.path.path) } }) { EmptyView() } .buttonStyle(CircleCheckboxButtonStyle(isSelected: selectedApps.contains(app.path.path))) } private var appContentView: some View { VStack { HStack { appIconAndName Divider() appSizeInfo Divider() savingsPercentage Spacer() sizesDisplay } .padding() } .background(ThemeColors.shared(for: colorScheme).secondaryBG) .clipShape(RoundedRectangle(cornerRadius: 8)) } private var appIconAndName: some View { HStack { if let icon = app.appIcon { Image(nsImage: icon) .resizable() .scaledToFit() .frame(width: 20, height: 20) } Text(app.appName).font(.title3) } } private var appSizeInfo: some View { Text(verbatim: "\(formatByte(size: Int64(app.bundleSize)).human)") .font(.caption) .help("Full app size") } @ViewBuilder private var savingsPercentage: some View { if let savings = calculatedSavings { if app.bundleSize > 0 && savings > 0 { Text("**\(Int((Double(savings) / Double(app.bundleSize)) * 100))%** savings") .font(.footnote) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } } @ViewBuilder private var sizesDisplay: some View { HStack { if let savings = calculatedSavings { Text(verbatim: "\(formatByte(size: Int64(savings)).human)") .foregroundStyle(.green) // .frame(minWidth: 100, alignment: .leading) .help("Potential savings from bundle thinning") } else if isCalculating { Text("Calculating...") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) // .frame(minWidth: 100, alignment: .leading) } else { Text("0 bytes") .foregroundStyle(.green) // .frame(minWidth: 100, alignment: .leading) .help("No savings available from bundle thinning") } } } private func calculateBundleSavings() { isCalculating = true Task { // Use our new bundle thinning approach to calculate potential savings let bundlePath = app.path let savings = await calculateBundleSavings(at: bundlePath) await MainActor.run { let wasAlreadyCalculated = calculatedSavings != nil calculatedSavings = savings // Update the shared state (always, even for 0 savings) sliceSizesByPath[app.path.path] = (bundle: UInt64(app.bundleSize), savings: savings) // Update totals (only add if not already counted) if !wasAlreadyCalculated { savingsAllApps += savings bundleAllApps += UInt64(app.bundleSize) } // Persist to AppState.sortedApps (in-memory only, no cache) if let index = AppState.shared.sortedApps.firstIndex(where: { $0.path == app.path }) { AppState.shared.sortedApps[index].lipoSavings = Int64(savings) } isCalculating = false } } } private func calculateBundleSavings(at bundlePath: URL) async -> UInt64 { return await withCheckedContinuation { continuation in DispatchQueue.global(qos: .utility).async { // Use the same function as actual lipo operation, but in dry-run mode let (success, sizes) = thinAppBundleArchitecture( at: bundlePath, of: app.arch, multi: true, dryRun: true) if success, let sizes = sizes { let preSize = sizes["pre"] ?? 0 let postSize = sizes["post"] ?? 0 let savings = preSize > postSize ? preSize - postSize : 0 continuation.resume(returning: savings) } else { continuation.resume(returning: 0) } } } } } ================================================ FILE: Pearcleaner/Views/MainWindow.swift ================================================ // // AppListH.swift // Pearcleaner // // Created by Alin Lupascu on 11/5/23. // import AlinFoundation import FinderSync import Foundation import SwiftUI struct MainWindow: View { @ObservedObject private var themeManager = ThemeManager.shared @ObservedObject private var consoleManager = GlobalConsoleManager.shared @StateObject private var brewManager = HomebrewManager() @StateObject private var updateManager = UpdateManager.shared @EnvironmentObject var appState: AppState @EnvironmentObject var locations: Locations @EnvironmentObject var fsm: FolderSettingsManager @EnvironmentObject var updater: Updater @EnvironmentObject var permissionManager: PermissionManagerLocal @Environment(\.colorScheme) var colorScheme @AppStorage("settings.general.glass") private var glass: Bool = false @AppStorage("settings.general.sidebarWidth") private var sidebarWidth: Double = 265 @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true @AppStorage("settings.tutorial.switchUtilitiesShown") private var tutorialShown: Bool = true @AppStorage("settings.updater.loadOnStartup") private var loadUpdatesOnStartup: Bool = true @AppStorage("settings.console.state") private var consoleStateData: Data = Data() @State private var isDraggingOver: Bool = false @State private var showSys: Bool = true @State private var showUsr: Bool = true @State private var showMenu = false @State private var isFullscreen = false // Badges @State private var showUpdateView = false @State private var showFeatureView = false @State private var showPermissionList = false @State private var glowRadius = 0.0 var body: some View { // Main App Window ZStack { HStack(alignment: .center, spacing: 0) { Group { switch appState.currentPage { case .applications: withConsole { applicationsView } case .orphans: withConsole { ZombieView() } case .development: withConsole { EnvironmentCleanerView() } case .lipo: withConsole { LipoView() } case .services: withConsole { DaemonView() } case .packages: withConsole { PackageView() } case .plugins: withConsole { PluginsView() } case .fileSearch: withConsole { FileSearchView() } case .homebrew: HomebrewView() .environmentObject(brewManager) case .updater: withConsole { AppsUpdaterView() .environmentObject(brewManager) .environmentObject(updateManager) } } } } // Drop overlay if isDraggingOver { ZStack { ThemeColors.shared(for: colorScheme).primaryBG .ignoresSafeArea() Image(systemName: "arrow.down") .font(.system(size: 100)) .foregroundColor(ThemeColors.shared(for: colorScheme).primaryText) .padding(40) .background( RoundedRectangle(cornerRadius: 20) .strokeBorder(style: StrokeStyle(lineWidth: 5, dash: [10, 5])) .foregroundColor(ThemeColors.shared(for: colorScheme).primaryText) ) } .transition(.opacity) } // Badge overlay (unified overlay for all badge notifications) BadgeOverlay() .environmentObject(updater) .zIndex(100) } .background(backgroundView(color: ThemeColors.shared(for: colorScheme).primaryBG)) .frame(minWidth: 900, minHeight: 650) .handlesExternalEvents(preferring: Set(arrayLiteral: "pear"), allowing: Set(arrayLiteral: "*")) .handleFileDrop( updater: updater, fsm: fsm, appState: appState, locations: locations, isTargeted: $isDraggingOver ) .onOpenURL(perform: { url in let deeplinkManager = DeeplinkManager(updater: updater, fsm: fsm) deeplinkManager.manage(url: url, appState: appState, locations: locations) }) .sheet(isPresented: $updater.sheet, content: { /// This will show the update sheet based on the frequency check function only updater.getUpdateView() }) .sheet(isPresented: $appState.showDeleteHistory, content: { DeleteHistoryView() .environmentObject(appState) .environmentObject(locations) .environmentObject(fsm) }) .onReceive( NotificationCenter.default.publisher(for: NSWindow.didEnterFullScreenNotification) ) { _ in isFullscreen = true } .onReceive( NotificationCenter.default.publisher(for: NSWindow.didExitFullScreenNotification) ) { _ in isFullscreen = false } .task { // Restore console state from AppStorage if let decoded = try? JSONDecoder().decode(ConsoleState.self, from: consoleStateData) { await MainActor.run { consoleManager.showConsole = decoded.isOpen consoleManager.consoleHeight = decoded.height } } } .onChange(of: consoleManager.showConsole) { newValue in // Save console state let state = ConsoleState(isOpen: newValue, height: consoleManager.consoleHeight) if let encoded = try? JSONEncoder().encode(state) { consoleStateData = encoded } // When console is hidden, trim output to 300 lines max to prevent memory bloat if !newValue { Task { @MainActor in consoleManager.trimOutput(toLines: 300) } } } .onChange(of: consoleManager.consoleHeight) { newValue in // Save console height when changed let state = ConsoleState(isOpen: consoleManager.showConsole, height: newValue) if let encoded = try? JSONEncoder().encode(state) { consoleStateData = encoded } } .toolbar { TahoeToolbarItem(placement: .navigation, isGroup: true) { // Page Selector Menu { ForEach(CurrentPage.availablePages, id: \.self) { page in Button { // Animate only the page content transition withAnimation(.easeInOut(duration: animationEnabled ? 0.3 : 0)) { // Reset appInfo when changing pages if page == .applications { appState.appInfo = .empty appState.currentView = .empty } } // Change page immediately (no animation on toolbar icon) appState.currentPage = page // Hide tutorial when user interacts with menu if tutorialShown { tutorialShown = false } } label: { if page == .updater { HStack(spacing: 8) { Image(systemName: page.icon) .frame(width: 16) if loadUpdatesOnStartup || updateManager.totalUpdateCount > 0 { Text(page.title) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .badge(updateManager.totalUpdateCount) } else { Text(page.title) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } } } else { HStack(spacing: 8) { Image(systemName: page.icon) .frame(width: 16) Text(page.title) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } } } } } label: { HStack { Image(systemName: appState.currentPage.icon) } } .menuIndicator(.hidden) if tutorialShown { HStack { Image(systemName: "arrowshape.left.fill") Text("Switch Utilities") } .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) .font(.caption) .padding(.horizontal, 8) .padding(.vertical, 4) .background( RoundedRectangle(cornerRadius: 6) .fill(ThemeColors.shared(for: colorScheme).secondaryBG) .overlay( RoundedRectangle(cornerRadius: 6) .strokeBorder( ThemeColors.shared(for: colorScheme).accent.opacity(0.5), lineWidth: 1) ) ) .onTapGesture { // Hide tutorial when user interacts with label tutorialShown = false } } // Notice Icons if updater.updateAvailable { noticeButton( image: "icloud.and.arrow.down.fill", color: .green, help: "Update Available" ) { showUpdateView.toggle() } .sheet(isPresented: $showUpdateView) { updater.getUpdateView() } } else if updater.announcementAvailable { noticeButton( image: "sparkles.2", color: .purple, help: "New Feature" ) { showFeatureView.toggle() } .sheet(isPresented: $showFeatureView) { updater.getAnnouncementView() } } else if permissionManager.shouldShowPermissionWarning { noticeButton( image: "lock.slash.fill", color: .red, help: "Permissions Missing" ) { showPermissionList.toggle() } .sheet(isPresented: $showPermissionList) { PermissionsSheetView() } } else if HelperToolManager.shared.shouldShowHelperBadge { noticeButton( image: "gear", color: .orange, help: "Helper Not Installed" ) { openAppSettingsWindow(tab: .helper, updater: updater) } } } } } @ViewBuilder private func noticeButton( image: String, color: Color, help: String, action: @escaping () -> Void ) -> some View { Button(action: action) { VStack(spacing: 4) { Image(systemName: image) .font(.system(size: 16, weight: .medium)) .foregroundColor(color) } .shadow(color: Color(NSColor.windowBackgroundColor).opacity(1), radius: 1, x: 0, y: 0) .shadow(color: color.opacity(1), radius: glowRadius, x: 0, y: 0) .animation(.easeInOut(duration: 1).repeatForever(autoreverses: true), value: glowRadius) } .buttonStyle(.plain) .help(help) .onAppear { glowRadius = 5.0 } } /// Helper to wrap view content with console at bottom @ViewBuilder private func withConsole(@ViewBuilder content: () -> Content) -> some View { VStack(spacing: 0) { content() if consoleManager.showConsole && !(appState.currentPage == .applications && appState.currentView == .empty) { GlobalConsoleView( output: consoleManager.consoleOutput, height: $consoleManager.consoleHeight, onClear: { Task { @MainActor in consoleManager.clearOutput() } } ) .frame(height: consoleManager.consoleHeight) .transition(.move(edge: .bottom)) } } } @ViewBuilder private var applicationsView: some View { HStack(alignment: .center, spacing: 0) { // App List AppSearchView() .frame(width: sidebarWidth) .transition(.opacity) .ifGlassMain() .padding([.leading, .vertical], 8) .ignoresSafeArea(edges: .top) // Details View with Console HStack(spacing: 0) { Group { switch appState.currentView { case .empty: MountedVolumeView() .id(appState.appInfo.id) case .files: FilesView() .id(appState.appInfo.id) .environmentObject(brewManager) } } .transition(.opacity) .frame(maxWidth: .infinity, maxHeight: .infinity) } .zIndex(2) } } } struct MountedVolumeView: View { @AppStorage("settings.interface.greetingEnabled") private var greetingEnabled: Bool = true @Environment(\.colorScheme) var colorScheme @ObservedObject private var themeManager = ThemeManager.shared @EnvironmentObject var appState: AppState @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true @AppStorage("settings.tutorial.dragToExpandShown") private var dragTutorialShown: Bool = true @State private var selectedVolumeIndex: Int = 0 // Debug sliders @State private var perspectiveValue: Double = 0.7 @State private var rotationValue: Double = 35.0 @State private var spacingValue: Double = 100.0 @State private var scaleValue: Double = 0.95 @State private var minOpacity: Double = 0.5 @State private var opacityFade: Double = 0.5 // @State private var debugMode: Bool = false var body: some View { ZStack(alignment: .center) { VStack { if greetingEnabled { ProfileMenuView() } Spacer() // Tutorial label for drag to expand (moved to end for proper z-order) if dragTutorialShown { HStack { HStack { Image(systemName: "arrowshape.left.fill") Text("Drag to expand into grid mode") } .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) .font(.caption) .padding(.horizontal, 8) .padding(.vertical, 4) .background( RoundedRectangle(cornerRadius: 6) .fill(ThemeColors.shared(for: colorScheme).secondaryBG) .overlay( RoundedRectangle(cornerRadius: 6) .strokeBorder( ThemeColors.shared(for: colorScheme).accent.opacity(0.5), lineWidth: 1) ) ) .onTapGesture { dragTutorialShown = false } Spacer() } } else { Text("Select an app from the sidebar to begin") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } if !appState.volumeInfos.isEmpty { ZStack { ForEach(Array(appState.volumeInfos.enumerated()), id: \.element.id) { index, volume in let offset = index - selectedVolumeIndex // Only show tiles within 1 position of center if abs(offset) <= 1 { let isCenter = offset == 0 let scale = isCenter ? 1.0 : scaleValue let opacity = isCenter ? 1.0 : minOpacity let yOffset = Double(offset) * spacingValue // 3D perspective skew - adjustable let perspective = isCenter ? 0.0 : (offset > 0 ? -perspectiveValue : perspectiveValue) let rotationX = isCenter ? 0.0 : (offset > 0 ? rotationValue : rotationValue) VolumeItemView(volume: volume, isCenter: isCenter, onEject: ejectVolume) .scaleEffect(scale) .opacity(opacity) .offset(y: yOffset) .rotation3DEffect( .degrees(rotationX), axis: (x: 1, y: 0, z: 0), perspective: perspective ) .shadow( color: isCenter ? .black.opacity(0.3) : .clear, radius: isCenter ? 10 : 0, x: 0, y: isCenter ? 5 : 0 ) .zIndex(isCenter ? 10 : Double(10 - abs(offset))) .onTapGesture { withAnimation( Animation.spring(response: 0.4, dampingFraction: 0.6) ) { selectedVolumeIndex = index } } .animation( animationEnabled ? .spring(response: 0.4, dampingFraction: 0.6) : .linear(duration: 0), value: selectedVolumeIndex) } } } } else { ProgressView() .controlSize(.small) } // Debug controls at bottom (only show if debug mode enabled) // if debugMode { // VStack { // Spacer() // VStack(spacing: 10) { // HStack { // Text("Perspective:") // Slider(value: $perspectiveValue, in: 0.0...1.0, step: 0.1) // Text(String(format: "%.1f", perspectiveValue)) // } // HStack { // Text("Rotation:") // Slider(value: $rotationValue, in: 0.0...45.0, step: 1.0) // Text(String(format: "%.0f°", rotationValue)) // } // HStack { // Text("Spacing:") // Slider(value: $spacingValue, in: 30.0...120.0, step: 5.0) // Text(String(format: "%.0f", spacingValue)) // } // HStack { // Text("Scale:") // Slider(value: $scaleValue, in: 0.3...1.0, step: 0.05) // Text(String(format: "%.2f", scaleValue)) // } // HStack { // Text("Min Opacity:") // Slider(value: $minOpacity, in: 0.1...0.9, step: 0.05) // Text(String(format: "%.2f", minOpacity)) // } // Button("Toggle Debug") { // debugMode = false // } // } // .padding() // .background(ThemeColors.shared(for: colorScheme).secondaryBG) // .cornerRadius(8) // .frame(maxWidth: 400) // } // } } .padding() .ignoresSafeArea(edges: .top) .onAppear { // Start with root volume (index 0) selected selectedVolumeIndex = 0 } .onChange(of: appState.isGridMode) { isGrid in // Hide tutorial when user switches to grid mode if isGrid { dragTutorialShown = false } } } private func ejectVolume(_ volume: VolumeInfo) { let workspace = NSWorkspace.shared let success = workspace.unmountAndEjectDevice(atPath: volume.path) if !success { printOS("Failed to eject volume: \(volume.name)") } else { // Find the current volume's index before ejection if let currentIndex = appState.volumeInfos.firstIndex(where: { $0.id == volume.id }) { // Refresh volume list after successful ejection appState.loadVolumeInfo() // Adjust selected index after volume removal DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { let newCount = appState.volumeInfos.count if newCount > 0 { selectedVolumeIndex = min(currentIndex, newCount - 1) } } } } } } struct ProfileMenuView: View { @Environment(\.colorScheme) var colorScheme @State private var showMenu = false @State private var profile: UserProfile? = nil var body: some View { HStack { Spacer() if let name = profile?.firstName { Text(name.lowercased()) .font(.largeTitle) .fontWeight(.thin) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } if let image = profile?.image { Button { // showMenu.toggle() } label: { Image(nsImage: image) .resizable() .scaledToFit() .frame(width: 40, height: 40) .clipShape(Circle()) .overlay { Circle() .strokeBorder( ThemeColors.shared(for: colorScheme).primaryText, lineWidth: 1 ) } } .buttonStyle(.plain) .allowsHitTesting(false) // .popover(isPresented: $showMenu, arrowEdge: .bottom) { // VStack(alignment: .leading, spacing: 12) { // Button("Profile Settings") { } // Button("Switch User") { } // Divider() // Button("Log Out", role: .destructive) { } // } // .padding() // .frame(width: 200) // } } } .onAppear { Task { profile = await getUserProfile() } } } } struct VolumeItemView: View { let volume: VolumeInfo let isCenter: Bool let onEject: (VolumeInfo) -> Void @Environment(\.colorScheme) var colorScheme @EnvironmentObject var appState: AppState @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true @State private var purgeableSize: Int64 = 0 @State private var usedSize: Int64 = 0 @State private var hoverAvailable: Bool = false @State private var hoverPurgeable: Bool = false @State private var hoverUsed: Bool = false @State private var isHovered: Bool = false @State private var isHoveredName: Bool = false var body: some View { VStack(alignment: .center, spacing: 20) { HStack(alignment: .center) { volume.icon .resizable() .scaledToFit() .frame(height: 70) .offset(y: 4) VStack(alignment: .leading) { HStack { HStack(spacing: 8) { Text(volume.name) .font(.title) .fontWeight(.bold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .underline(isHoveredName) .onTapGesture { NSWorkspace.shared.open( URL( string: "x-apple.systempreferences:com.apple.settings.Storage" )!) } .onHover(perform: { isHovered in self.isHoveredName = isHovered }) if volume.isExternal { Button(action: { onEject(volume) }) { Image(systemName: "eject") .font(.title3) .foregroundStyle( ThemeColors.shared(for: colorScheme).secondaryText) } .buttonStyle(PlainButtonStyle()) } } Spacer() let percentUsed = Double(volume.usedSpace) / Double(volume.totalSpace) * 100 Text(String(format: "%.0f%% full", percentUsed)) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .offset(y: -5) } .padding(.bottom, 4) HStack { Text("Location:") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text(verbatim: "\(volume.path)") .font(.subheadline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } HStack { Text("Available:") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text( ByteCountFormatter.string( fromByteCount: volume.realAvailableSpace, countStyle: .file) ) .font(.subheadline) .foregroundStyle( hoverAvailable ? Color.green : ThemeColors.shared(for: colorScheme).primaryText ) .animation(.easeInOut(duration: 0.2), value: hoverAvailable) } if volume.purgeableSpace > 0 { HStack { Text("Purgeable:") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text( ByteCountFormatter.string( fromByteCount: volume.purgeableSpace, countStyle: .file) ) .font(.subheadline) .foregroundStyle( hoverPurgeable ? ThemeColors.shared(for: colorScheme).accent : ThemeColors.shared(for: colorScheme).primaryText ) .animation(.easeInOut(duration: 0.2), value: hoverPurgeable) } .help( "Purgeable space refers to the System Data taken up by macOS. This cannot be manually freed and is automatically managed by your system." ) } } } HStack(alignment: .center) { Text(ByteCountFormatter.string(fromByteCount: volume.usedSpace, countStyle: .file)) .font(.caption) .foregroundStyle( hoverUsed ? ThemeColors.shared(for: colorScheme).accent : ThemeColors.shared(for: colorScheme).secondaryText ) .offset(y: -1) GeometryReader { geo in ZStack(alignment: .leading) { RoundedRectangle(cornerRadius: 5) .fill(ThemeColors.shared(for: colorScheme).primaryBG) RoundedRectangle(cornerRadius: 5) .fill(ThemeColors.shared(for: colorScheme).accent) .brightness(-0.3) .saturation(0.5) .padding(3) .frame( width: geo.size.width * CGFloat(purgeableSize) / CGFloat(volume.totalSpace) ) .animation( animationEnabled && !volume.hasAnimated ? .spring(response: 0.7, dampingFraction: 0.6, blendDuration: 0) : .linear(duration: 0), value: purgeableSize ) .help( "Purgeable space refers to the System Data taken up by macOS. This cannot be manually freed and is automatically managed by your system." ) RoundedRectangle(cornerRadius: 5) .fill(ThemeColors.shared(for: colorScheme).accent) .padding(3) .frame( width: geo.size.width * CGFloat(usedSize) / CGFloat(volume.totalSpace) ) .animation( animationEnabled && !volume.hasAnimated ? .spring(response: 0.7, dampingFraction: 0.6, blendDuration: 0) : .linear(duration: 0), value: usedSize) HStack(spacing: 0) { Rectangle() .fill(Color.clear) .frame( width: geo.size.width * CGFloat(volume.usedSpace) / CGFloat(volume.totalSpace), height: 10 ) .onHover { hovering in hoverUsed = hovering } Rectangle() .fill(Color.clear) .frame( width: geo.size.width * CGFloat(volume.purgeableSpace) / CGFloat(volume.totalSpace), height: 10 ) .onHover { hovering in hoverPurgeable = hovering } Rectangle() .fill(Color.clear) .frame(height: 10) .onHover { hovering in hoverAvailable = hovering } Spacer() } } } Text(ByteCountFormatter.string(fromByteCount: volume.totalSpace, countStyle: .file)) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .offset(y: -1) } .frame(height: 10) } .padding() .background( RoundedRectangle(cornerRadius: 12) .fill(ThemeColors.shared(for: colorScheme).secondaryBG) ) .frame(maxWidth: 500) .disabled(!isCenter) .scaleEffect((!isCenter && isHovered) ? 1.01 : 1.0) .onHover { hovering in if !isCenter { withAnimation(.easeInOut(duration: 0.2)) { isHovered = hovering } } } .onChange(of: isCenter) { centered in if centered { // Clear hover state when becoming center isHovered = false } } .onAppear { if volume.hasAnimated { purgeableSize = volume.usedSpace + volume.purgeableSpace usedSize = volume.usedSpace } else if isCenter { startVolumeAnimation() } else { purgeableSize = 0 usedSize = 0 } } .onChange(of: isCenter) { centered in if centered && !volume.hasAnimated { startVolumeAnimation() } else if !centered { purgeableSize = volume.usedSpace + volume.purgeableSpace usedSize = volume.usedSpace } } } private func startVolumeAnimation() { purgeableSize = 0 usedSize = 0 if animationEnabled { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { self.purgeableSize = volume.usedSpace + volume.purgeableSpace } DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { self.usedSize = volume.usedSpace self.markVolumeAsAnimated() } } else { self.purgeableSize = volume.usedSpace + volume.purgeableSpace self.usedSize = volume.usedSpace self.markVolumeAsAnimated() } } private func markVolumeAsAnimated() { if let index = appState.volumeInfos.firstIndex(where: { $0.id == volume.id }) { appState.volumeInfos[index].hasAnimated = true } } } ================================================ FILE: Pearcleaner/Views/PackageView.swift ================================================ // // PackageView.swift // Pearcleaner // // Created by Alin Lupascu on 8/10/25. // import SwiftUI import AlinFoundation enum PackageSortOption: String, CaseIterable { case packageName = "Name" case packageId = "ID" case installer = "Installer" var displayName: String { return self.rawValue } var systemImage: String { switch self { case .packageName: return "list.bullet" case .packageId: return "number" case .installer: return "app.badge" } } } struct PackageInfo: Identifiable, Hashable, Equatable { let id = UUID() let packageId: String let packageName: String let packageFileName: String let version: String let installDate: String let installProcessName: String var bomFiles: [String] let receiptPath: String let installLocation: String var bomFilesLoaded: Bool = false // NEW: Additional metadata from private PKG APIs let packageGroups: [String] // Groups this package belongs to let additionalInfo: String // Extra package information let isSecure: Bool // Whether package is signed/secure let receiptStoragePaths: [String] // All receipt file paths let totalSizeFromBOM: Int64 // Total installed size from BOM let totalFilesInBOM: Int // Total file count from BOM var displayName: String { if !packageFileName.isEmpty { // Remove .pkg extension for cleaner display let name = packageFileName.hasSuffix(".pkg") ? String(packageFileName.dropLast(4)) : packageFileName return name } else if !packageName.isEmpty { return packageName } else { return packageId } } } struct PackageView: View { @EnvironmentObject var appState: AppState @Environment(\.colorScheme) var colorScheme @ObservedObject private var consoleManager = GlobalConsoleManager.shared @State private var packages: [PackageInfo] = [] @State private var packageIds: [String] = [] @State private var isLoading: Bool = false @State private var lastRefreshDate: Date? @State private var searchText: String = "" @State private var expandedPackages: Set = [] @State private var sortOption: PackageSortOption = .packageName @AppStorage("settings.interface.scrollIndicators") private var scrollIndicators: Bool = false // Uninstall sheet state @State private var uninstallSheetWindow: NSWindow? @State private var packageToUninstall: PackageInfo? @State private var filesToUninstall: [String] = [] @State private var selectedFilesToUninstall: Set = [] private var filteredPackages: [PackageInfo] { var packages = self.packages.filter { !$0.packageId.hasPrefix("com.apple.") } if !searchText.isEmpty { packages = packages.filter { package in package.displayName.localizedCaseInsensitiveContains(searchText) || package.packageId.localizedCaseInsensitiveContains(searchText) || package.version.localizedCaseInsensitiveContains(searchText) } } switch sortOption { case .packageName: packages = packages.sorted { first, second in return first.displayName.localizedCaseInsensitiveCompare(second.displayName) == .orderedAscending } case .packageId: packages = packages.sorted { first, second in return first.packageId.localizedCaseInsensitiveCompare(second.packageId) == .orderedAscending } case .installer: packages = packages.sorted { first, second in // Handle empty installer names by putting them at the end if first.installProcessName.isEmpty && second.installProcessName.isEmpty { return first.displayName.localizedCaseInsensitiveCompare(second.displayName) == .orderedAscending } else if first.installProcessName.isEmpty { return false } else if second.installProcessName.isEmpty { return true } else { return first.installProcessName.localizedCaseInsensitiveCompare(second.installProcessName) == .orderedAscending } } } return packages } var body: some View { VStack(alignment: .leading, spacing: 0) { // Search bar HStack { Image(systemName: "magnifyingglass") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) TextField("Search...", text: $searchText) .textFieldStyle(.plain) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) if !searchText.isEmpty { Button { searchText = "" } label: { Image(systemName: "xmark.circle.fill") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .buttonStyle(.plain) } } .padding(.horizontal, 12) .padding(.vertical, 8) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) .controlGroup(Capsule(style: .continuous), level: .primary) .padding(.top, 5) if isLoading && packages.isEmpty { VStack(alignment: .center, spacing: 10) { Spacer() ProgressView() .scaleEffect(1.5) Text("Loading packages...") .font(.title2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() } .frame(maxWidth: .infinity) } else if filteredPackages.isEmpty && !isLoading { VStack(alignment: .center) { Spacer() Text("No packages found") .font(.title2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() } .frame(maxWidth: .infinity) } else { // Stats header HStack { Text("\(filteredPackages.count) package\(filteredPackages.count == 1 ? "" : "s")") .font(.caption) .monospacedDigit() .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) if isLoading { Text("Loading...") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } Spacer() if let lastRefresh = lastRefreshDate { TimelineView(.periodic(from: lastRefresh, by: 1.0)) { _ in Text("Updated \(formatRelativeTime(lastRefresh))") .font(.caption) .monospacedDigit() .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } } .padding(.vertical) ScrollView { LazyVStack(spacing: 8) { ForEach(filteredPackages, id: \.id) { package in PackageRowView( package: package, isExpanded: expandedPackages.contains(package.packageId), sortOption: sortOption ) { toggleExpansion(for: package.packageId) } onForget: { forgetPackage(package) } onUninstall: { uninstallPackage(package) } onRefresh: { refreshPackages() } onUpdateBomFiles: { updatedBomFiles in // Update the local BOM files for this package if let packageIndex = packages.firstIndex(where: { $0.packageId == package.packageId }) { packages[packageIndex].bomFiles = updatedBomFiles } } .onAppear { loadBOMFilesIfNeeded(for: package.packageId) } } } } .scrollIndicators(scrollIndicators ? .automatic : .never) } } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding([.horizontal], 20) .onAppear { if packages.isEmpty { refreshPackages() } } .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("PackagesViewShouldRefresh"))) { _ in // Refresh packages refreshPackages() } .toolbarBackground(.hidden, for: .windowToolbar) .toolbar { TahoeToolbarItem(placement: .navigation) { VStack(alignment: .leading) { Text("Package Manager") .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .font(.title2) .fontWeight(.bold) Text("Manage packages installed via macOS Installer") .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } ToolbarItem { Spacer() } TahoeToolbarItem(isGroup: true) { Button { withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) { consoleManager.showConsole.toggle() } } label: { Label("Console", systemImage: consoleManager.showConsole ? "terminal.fill" : "terminal") } .help("Toggle console output") Menu { ForEach(PackageSortOption.allCases, id: \.self) { option in Button { sortOption = option } label: { Label(option.displayName, systemImage: option.systemImage) } } } label: { Label(sortOption.displayName, systemImage: sortOption.systemImage) } .labelStyle(.titleAndIcon) .menuIndicator(.hidden) Button { refreshPackages() } label: { Label("Refresh", systemImage: "arrow.counterclockwise") } .disabled(isLoading) } } } private func toggleExpansion(for packageId: String) { if expandedPackages.contains(packageId) { expandedPackages.remove(packageId) } else { expandedPackages.insert(packageId) } } private func loadBOMFilesIfNeeded(for packageId: String) { // Find the package and check if BOM files are already loaded guard let packageIndex = packages.firstIndex(where: { $0.packageId == packageId }), !packages[packageIndex].bomFilesLoaded else { return } Task { let bomFiles = await loadBOMFiles(for: packageId) await MainActor.run { if packageIndex < packages.count { packages[packageIndex].bomFiles = bomFiles packages[packageIndex].bomFilesLoaded = true } } } } private func loadBOMFiles(for packageId: String) async -> [String] { return await withCheckedContinuation { continuation in DispatchQueue.global(qos: .userInitiated).async { if #available(macOS 10.5, *) { // Find the package by ID guard let package = self.packages.first(where: { $0.packageId == packageId }) else { continuation.resume(returning: []) return } // Get all receipts to find the one matching this package ID let receipts = PKGManager.getAllPackages(volume: "/") guard let receipt = receipts.first(where: { ($0.packageIdentifier() as? String) == packageId }) else { continuation.resume(returning: []) return } // Use PKG API to get files from BOM let files = PKGManager.getPackageFiles(receipt: receipt, installLocation: package.installLocation) // Filter out app bundle internals, keep only top-level .app paths let filteredFiles = filterAppBundleInternals(files) continuation.resume(returning: filteredFiles) } else { // Fallback for older macOS continuation.resume(returning: []) } } } } private func refreshPackages() { GlobalConsoleManager.shared.appendOutput("Refreshing packages...\n", source: CurrentPage.packages.title) isLoading = true packages = [] packageIds = [] Task { let loadedPackages = await loadPackagesFromPKGAPI() await MainActor.run { self.packages = loadedPackages self.packageIds = loadedPackages.map { $0.packageId } self.lastRefreshDate = Date() self.isLoading = false GlobalConsoleManager.shared.appendOutput("✓ Loaded \(loadedPackages.count) packages\n", source: CurrentPage.packages.title) } } } private func loadPackagesFromPKGAPI() async -> [PackageInfo] { return await withCheckedContinuation { continuation in DispatchQueue.global(qos: .userInitiated).async { if #available(macOS 10.5, *) { // Use private PKG API to get all receipts let receipts = PKGManager.getAllPackages(volume: "/") // Convert receipts to PackageInfo objects let packages = receipts.compactMap { receipt in PKGManager.getPackageInfo(from: receipt) } continuation.resume(returning: packages) } else { // Fallback for older macOS (shouldn't happen) continuation.resume(returning: []) } } } } private func forgetPackage(_ package: PackageInfo) { GlobalConsoleManager.shared.appendOutput("Starting forget operation for \(package.displayName)...\n", source: CurrentPage.packages.title) Task { await performPackageForget(package) } } private func uninstallPackage(_ package: PackageInfo) { GlobalConsoleManager.shared.appendOutput("Starting uninstall operation for \(package.displayName)...\n", source: CurrentPage.packages.title) Task { await prepareUninstall(package) } } private func performPackageForget(_ package: PackageInfo) async { let success = await withCheckedContinuation { continuation in DispatchQueue.global(qos: .userInitiated).async { // Check if app bundle still exists in common locations let appBundleName = self.extractAppBundleName(from: package.bomFiles) if let bundleName = appBundleName { let commonAppPaths = [ "/Applications/\(bundleName)", "/System/Applications/\(bundleName)", "~/Applications/\(bundleName)".expandingTildeInPath ] for appPath in commonAppPaths { if FileManager.default.fileExists(atPath: appPath) { DispatchQueue.main.async { showCustomAlert( title: "App Bundle Still Exists", message: "The app '\(bundleName)' still exists in \(appPath). Please use the Apps tab to fully remove the app and its related files first, then return here to forget the package.", style: .warning ) } continuation.resume(returning: false) return } } } // Get receipt file paths to delete directly (no shell command needed) let receiptPaths = package.receiptStoragePaths if receiptPaths.isEmpty { // printOS("⚠️ No receipt paths found for package \(package.packageName)") continuation.resume(returning: false) return } // Use FileManagerUndo to safely delete receipt files to trash let receiptURLs = receiptPaths.map { URL(fileURLWithPath: $0) } let success = FileManagerUndo.shared.deleteFiles(at: receiptURLs, bundleName: "PKG-\(package.packageName)") if !success { printOS("Failed to forget package.") } continuation.resume(returning: success) } } await MainActor.run { if success { GlobalConsoleManager.shared.appendOutput("✓ Completed forget operation for \(package.displayName)\n", source: CurrentPage.packages.title) // Remove the package from the local array packages.removeAll { $0.packageId == package.packageId } packageIds.removeAll { $0 == package.packageId } // Also remove from expanded packages if it was expanded expandedPackages.remove(package.packageId) } else { showCustomAlert( title: "Forget Failed", message: "Failed to forget package '\(package.displayName)'. The package may require additional permissions or may not exist.", style: .critical ) } } } private func prepareUninstall(_ package: PackageInfo) async { // Ensure BOM files are loaded before showing the sheet var updatedPackage = package if !package.bomFilesLoaded { // Show sheet with loading state await MainActor.run { self.packageToUninstall = package self.filesToUninstall = [] self.selectedFilesToUninstall = [] self.showUninstallSheet(package: package, files: []) } // Load BOM files in background let bomFiles = await loadBOMFiles(for: package.packageId) // Update package with loaded files await MainActor.run { if let index = packages.firstIndex(where: { $0.packageId == package.packageId }) { packages[index].bomFiles = bomFiles packages[index].bomFilesLoaded = true updatedPackage = packages[index] } } } let (existingFiles, _) = getBomFilesByExistence(for: updatedPackage) await MainActor.run { if existingFiles.isEmpty { // Close the sheet if it was open if let sheetWindow = self.uninstallSheetWindow, let parentWindow = NSApp.keyWindow { parentWindow.endSheet(sheetWindow) } self.uninstallSheetWindow = nil showCustomAlert( title: "No Files Found", message: "No files found to uninstall for package '\(package.displayName)'.\n\nWould you like to forget this package? This will remove it from system records only.", style: .informational, onOk: { forgetPackage(package) } ) return } // Set all data self.packageToUninstall = updatedPackage self.filesToUninstall = existingFiles self.selectedFilesToUninstall = Set(existingFiles) // All checked by default // Show the sheet self.showUninstallSheet(package: updatedPackage, files: existingFiles) } } private func showUninstallSheet(package: PackageInfo, files: [String]) { guard let parentWindow = NSApp.keyWindow ?? NSApp.windows.first(where: { $0.isVisible }) else { return } // Create the SwiftUI view let contentView = PackageUninstallSheet( package: package, files: files, selectedFiles: $selectedFilesToUninstall, onConfirm: { if let sheetWindow = self.uninstallSheetWindow { parentWindow.endSheet(sheetWindow) } self.uninstallSheetWindow = nil Task { await performFullUninstall(package, selectedFiles: Array(self.selectedFilesToUninstall)) } }, onCancel: { if let sheetWindow = self.uninstallSheetWindow { parentWindow.endSheet(sheetWindow) } self.uninstallSheetWindow = nil } ) // If sheet already exists, update its content if let existingSheet = uninstallSheetWindow, let hostingController = existingSheet.contentViewController as? NSHostingController { hostingController.rootView = contentView return } // Create new sheet window let hostingController = NSHostingController(rootView: contentView) let sheetWindow = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 600, height: 500), styleMask: [.titled, .closable], backing: .buffered, defer: false ) sheetWindow.title = "Uninstall Package" sheetWindow.contentViewController = hostingController sheetWindow.isReleasedWhenClosed = false // Present as sheet parentWindow.beginSheet(sheetWindow) self.uninstallSheetWindow = sheetWindow } private func performFullUninstall(_ package: PackageInfo, selectedFiles: [String]) async { // Step 1: Delete BOM files using FileManagerUndo (moves to Trash with undo support) let bomFilesSuccess = await withCheckedContinuation { continuation in DispatchQueue.global(qos: .userInitiated).async { if selectedFiles.isEmpty { continuation.resume(returning: true) return } // Convert file paths to URLs let urls = selectedFiles.map { URL(fileURLWithPath: $0) } // Use FileManagerUndo to move files to Trash (supports undo) let bundleName = "Package - \(package.displayName)" let success = FileManagerUndo.shared.deleteFiles(at: urls, bundleName: bundleName) continuation.resume(returning: success) } } // Step 2: Delete receipt files (must use privileged commands as they're system files) let receiptsSuccess = await withCheckedContinuation { continuation in DispatchQueue.global(qos: .userInitiated).async { let receiptPaths = package.receiptStoragePaths guard !receiptPaths.isEmpty else { continuation.resume(returning: true) return } // Use FileManagerUndo to safely delete receipt files to trash let receiptURLs = receiptPaths.map { URL(fileURLWithPath: $0) } let success = FileManagerUndo.shared.deleteFiles(at: receiptURLs, bundleName: "PKG-\(package.packageId)") continuation.resume(returning: success) } } await MainActor.run { if bomFilesSuccess && receiptsSuccess { // Remove the package from the local array packages.removeAll { $0.packageId == package.packageId } packageIds.removeAll { $0 == package.packageId } // Also remove from expanded packages if it was expanded expandedPackages.remove(package.packageId) } else { var message = "Failed to fully uninstall package '\(package.displayName)'." if !bomFilesSuccess { message += " Some files could not be deleted." } if !receiptsSuccess { message += " Receipt files could not be removed." } showCustomAlert( title: "Uninstall Failed", message: message, style: .critical ) } } } private func extractAppBundleName(from bomFiles: [String]) -> String? { // Look for .app bundle in the BOM files for file in bomFiles { if file.contains(".app/") { let components = file.components(separatedBy: "/") for component in components { if component.hasSuffix(".app") { return component } } } } return nil } } struct PackageRowView: View { @Environment(\.colorScheme) var colorScheme let package: PackageInfo let isExpanded: Bool let sortOption: PackageSortOption let onToggleExpansion: () -> Void let onForget: () -> Void let onUninstall: () -> Void let onRefresh: () -> Void let onUpdateBomFiles: ([String]) -> Void @State private var isPerformingAction = false @State private var isHovered = false @AppStorage("settings.interface.scrollIndicators") private var scrollIndicators: Bool = false var body: some View { VStack(alignment: .leading, spacing: 12) { // Main package info row HStack(alignment: .top, spacing: 12) { // Package icon and type indicator VStack(spacing: 4) { ZStack { Circle() .fill(packageColor.opacity(0.2)) .frame(width: 32, height: 32) Image(systemName: "shippingbox.fill") .font(.system(size: 14, weight: .medium)) .foregroundStyle(packageColor) } } // Package details VStack(alignment: .leading, spacing: 6) { HStack(alignment: .center, spacing: 8) { if sortOption == .packageId { Text(package.packageId) .font(.headline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .lineLimit(1) } else { Text(package.displayName) .font(.headline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .lineLimit(1) } // Security badge if package.isSecure { HStack(spacing: 2) { Image(systemName: "lock.fill") .font(.caption2) Text("Secure") .font(.caption2) } .foregroundStyle(.green) .help("This package is signed and verified. The package was cryptographically signed by the developer and its integrity has been verified.") } else { HStack(spacing: 2) { Image(systemName: "exclamationmark.triangle") .font(.caption2) Text("Unsigned") .font(.caption2) } .foregroundStyle(.orange) .help("This package is unsigned or unverified. The package was not cryptographically signed, or its signature could not be verified.") } Spacer() } // Package details VStack(alignment: .leading, spacing: 4) { if sortOption == .packageId { Text("Name: \(package.displayName)") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .lineLimit(1) .truncationMode(.middle) } else { Text("ID: \(package.packageId)") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .lineLimit(1) .truncationMode(.middle) } // Version, file count, size HStack { Text("Version: \(package.version)") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) if package.totalFilesInBOM > 0 { Text(verbatim: "•") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text("\(package.totalFilesInBOM) files") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } if package.totalSizeFromBOM > 0 { Text(verbatim: "•") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text(formatBytes(package.totalSizeFromBOM)) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } Spacer() } // Package groups HStack(spacing: 6) { if !package.packageGroups.isEmpty { ForEach(package.packageGroups.prefix(3), id: \.self) { group in Text(formatGroupName(group)) .font(.caption2) .padding(.horizontal, 6) .padding(.vertical, 2) .background(Color.blue.opacity(0.2)) .foregroundStyle(Color.blue) .cornerRadius(4) } if package.packageGroups.count > 3 { Text(verbatim: "+\(package.packageGroups.count - 3)") .font(.caption2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } Spacer() } // Install date if !package.installDate.isEmpty { HStack { Text("Installed:") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text(formatInstallDate(package.installDate)) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() } } // Install location (if not default) if !package.installLocation.isEmpty && package.installLocation != "/" { HStack { Text("Location:") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text(package.installLocation.hasPrefix("/") ? package.installLocation : "/" + package.installLocation) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .lineLimit(1) .truncationMode(.middle) Spacer() } } // Install process (if available) if !package.installProcessName.isEmpty { HStack { Text("Installer:") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text(package.installProcessName) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() } } } } // Action buttons VStack(spacing: 6) { HStack(spacing: 10) { Button(isExpanded ? "Close" : "Details") { onToggleExpansion() } .buttonStyle(.borderless) .controlSize(.mini) .foregroundStyle(.blue) .disabled(isPerformingAction) .help(isExpanded ? "Hide details" : "Show file details") Divider().frame(height: 10) Button("Forget") { onForget() } .buttonStyle(.borderless) .controlSize(.mini) .foregroundStyle(.orange) .disabled(isPerformingAction) .help("Remove package from system records (does not delete files)") Divider().frame(height: 10) Button("Uninstall") { onUninstall() } .buttonStyle(.borderless) .controlSize(.mini) .foregroundStyle(.red) .disabled(isPerformingAction) .help("Completely remove package: delete all files, receipts, and forget package") } if isPerformingAction { ProgressView() .scaleEffect(0.7) } } } // Expanded details if isExpanded { Divider() VStack(alignment: .leading, spacing: 8) { HStack(spacing: 0) { InfoButton(text: "By default, pkgutil shows all file paths from the package which might include system paths like /Library/Application Support, /usr/local, etc. and even files that are already deleted from the system.\n\nThis list of BOM files is filtered as follows:\n- Shows directories that are not system directories. \n- Doesn't show files that are duplicated by listing out all contents of parent directories.\n- Doesn't show files that are already deleted.") Text("Bill of Materials") .font(.headline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .padding(.horizontal, 5) if package.bomFilesLoaded { let (existingFiles, _) = getBomFilesByExistence(for: package) let count = existingFiles.count Text("\(count) valid \(count == 1 ? "file" : "files") found") .font(.caption2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() if count > 1 { Button("Remove All") { removeAllBomFiles() } .buttonStyle(.borderless) .controlSize(.mini) .foregroundStyle(.red) .disabled(isPerformingAction) .help("Delete all remaining package files") } } } // Additional info (if available) if !package.additionalInfo.isEmpty { VStack(alignment: .leading, spacing: 4) { Text("Additional Info:") .font(.caption) .fontWeight(.semibold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Text(package.additionalInfo) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .padding(8) .background(ThemeColors.shared(for: colorScheme).secondaryBG.opacity(0.3)) .cornerRadius(6) } // Receipt information VStack(alignment: .leading, spacing: 4) { HStack { Text("Receipt Path:") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text(package.receiptPath) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .lineLimit(1) .truncationMode(.middle) Spacer() } if !package.installLocation.isEmpty { HStack { Text("Install Location:") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text(package.installLocation.hasPrefix("/") ? package.installLocation : "/" + package.installLocation) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .lineLimit(1) .truncationMode(.middle) Spacer() } } } // Receipt storage paths (collapsible) if package.receiptStoragePaths.count > 1 { DisclosureGroup { VStack(alignment: .leading, spacing: 2) { ForEach(package.receiptStoragePaths, id: \.self) { path in Text(path) .font(.caption2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .frame(maxWidth: .infinity, alignment: .leading) } } .frame(maxWidth: .infinity, alignment: .leading) .padding(.leading, 8) .padding(.top, 4) } label: { Text("Receipt Storage Paths (\(package.receiptStoragePaths.count))") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .padding(.bottom, 8) } if !package.bomFilesLoaded { HStack { ProgressView() .scaleEffect(0.8) Text("Loading files...") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .frame(height: 40) } else { let (existingFiles, _) = getBomFilesByExistence(for: package) if existingFiles.isEmpty { VStack(alignment: .leading, spacing: 8) { Text("All valid package files from the BOM list have been removed. You may Forget this package.") .font(.caption2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .opacity(0.8) } .frame(height: 60) .padding(.horizontal, 8) } else { ScrollView { LazyVStack(alignment: .leading, spacing: 4) { ForEach(Array(existingFiles.enumerated()), id: \.offset) { index, file in BomFileRowView( file: file, exists: true, index: index, colorScheme: colorScheme, onView: { openFileInFinder(file) }, onRemove: { removeFile(file, from: package) } ) } } } .frame(maxHeight: 200) .scrollIndicators(scrollIndicators ? .automatic : .never) .background(ThemeColors.shared(for: colorScheme).secondaryBG.opacity(0.5)) .cornerRadius(6) } } } } } .padding() .background( RoundedRectangle(cornerRadius: 8) .fill(isHovered ? ThemeColors.shared(for: colorScheme).secondaryBG.opacity(0.8) : ThemeColors.shared(for: colorScheme).secondaryBG ) ) .onTapGesture { onToggleExpansion() } .onHover { hovering in withAnimation(.easeInOut(duration: 0.2)) { isHovered = hovering } } } private var packageColor: Color { return .green } private func formatInstallDate(_ dateString: String) -> String { // Parse the install date format from pkgutil if let timestamp = Double(dateString) { let date = Date(timeIntervalSince1970: timestamp) let formatter = DateFormatter() formatter.dateStyle = .medium formatter.timeStyle = .short return formatter.string(from: date) } else { return dateString } } private func formatBytes(_ bytes: Int64) -> String { let formatter = ByteCountFormatter() formatter.allowedUnits = [.useAll] formatter.countStyle = .file return formatter.string(fromByteCount: bytes) } private func formatGroupName(_ group: String) -> String { // Remove common prefixes to make badges more readable let cleanedGroup = group .replacingOccurrences(of: "com.apple.group.", with: "") .replacingOccurrences(of: "com.apple.", with: "") return cleanedGroup } private func openFileInFinder(_ filePath: String) { let url = URL(fileURLWithPath: filePath) NSWorkspace.shared.activateFileViewerSelecting([url]) } private func removeFile(_ filePath: String, from package: PackageInfo) { Task { await performFileRemoval(filePath, from: package) } } private func performFileRemoval(_ filePath: String, from package: PackageInfo) async { let success = await withCheckedContinuation { continuation in DispatchQueue.global(qos: .userInitiated).async { // Use FileManagerUndo to safely delete file to trash let fileURL = URL(fileURLWithPath: filePath) let success = FileManagerUndo.shared.deleteFiles(at: [fileURL], bundleName: "PKG-\(package.packageId)") continuation.resume(returning: success) } } await MainActor.run { if success { // Update BOM files locally using callback let updatedBomFiles = package.bomFiles.filter { $0 != filePath } onUpdateBomFiles(updatedBomFiles) } else { showCustomAlert( title: "Removal Failed", message: "Failed to remove '\(filePath)'. The file may require additional permissions or may not exist.", style: .critical ) } } } private func removeAllBomFiles() { let filteredFiles = getFilteredBomFiles(for: package) if filteredFiles.isEmpty { showCustomAlert( title: "No Files to Remove", message: "There are no package files remaining on the system to remove.", style: .informational ) return } Task { await performBulkFileRemoval(filteredFiles) } } private func performBulkFileRemoval(_ filePaths: [String]) async { let success = await withCheckedContinuation { continuation in DispatchQueue.global(qos: .userInitiated).async { // Use FileManagerUndo to safely delete files to trash let fileURLs = filePaths.map { URL(fileURLWithPath: $0) } let success = FileManagerUndo.shared.deleteFiles(at: fileURLs, bundleName: "PKG-\(package.packageId)") continuation.resume(returning: success) } } await MainActor.run { isPerformingAction = false if success { // Update BOM files locally using callback let updatedBomFiles = package.bomFiles.filter { !filePaths.contains($0) } onUpdateBomFiles(updatedBomFiles) } else { showCustomAlert( title: "Removal Failed", message: "Failed to remove some or all package files. Some files may require additional permissions or may not exist.", style: .critical ) } } } } private func getFilteredBomFiles(for package: PackageInfo) -> [String] { let (existingFiles, _) = getBomFilesByExistence(for: package) return existingFiles } // Helper function to run shell commands private func runDirectShellCommand(command: String) -> (Bool, String) { let task = Process() task.launchPath = "/bin/sh" task.arguments = ["-c", command] let pipe = Pipe() task.standardOutput = pipe task.standardError = pipe task.launch() task.waitUntilExit() let data = pipe.fileHandleForReading.readDataToEndOfFile() let output = String(data: data, encoding: .utf8) ?? "" return (task.terminationStatus == 0, output) } private func runDirectShellCommandWithTimeout(command: String, timeout: TimeInterval) -> (Bool, String) { let task = Process() task.launchPath = "/bin/bash" task.arguments = ["-c", command] let pipe = Pipe() task.standardOutput = pipe task.standardError = pipe task.launch() let group = DispatchGroup() group.enter() var completed = false var result: (Bool, String) = (false, "") DispatchQueue.global().async { task.waitUntilExit() let data = pipe.fileHandleForReading.readDataToEndOfFile() let output = String(data: data, encoding: .utf8) ?? "" if !completed { result = (task.terminationStatus == 0, output) } completed = true group.leave() } let waitResult = group.wait(timeout: .now() + timeout) if waitResult == .timedOut { task.terminate() return (false, "Command timed out") } return result } private func runFastShellCommand(command: String) -> (Bool, String) { let tempFile = "/tmp/pearcleaner_getBOMlist_\(UUID().uuidString)" let redirectedCommand = "\(command) > \"\(tempFile)\" 2>&1" let process = Process() process.executableURL = URL(fileURLWithPath: "/bin/bash") process.arguments = ["-c", redirectedCommand] do { try process.run() process.waitUntilExit() let success = process.terminationStatus == 0 let output: String if FileManager.default.fileExists(atPath: tempFile) { output = (try? String(contentsOfFile: tempFile)) ?? "" try? FileManager.default.removeItem(atPath: tempFile) } else { output = "" } return (success, output) } catch { try? FileManager.default.removeItem(atPath: tempFile) return (false, "Error: \(error)") } } extension String { var expandingTildeInPath: String { return NSString(string: self).expandingTildeInPath } } private func filterAppBundleInternals(_ files: [String]) -> [String] { var appBundles: Set = [] var filteredFiles: [String] = [] for file in files { if file.hasSuffix(".app") { appBundles.insert(file) } } // Filter out files that are inside .app bundles, keep only the .app bundle itself for file in files { var shouldInclude = true for appBundle in appBundles { if file.hasPrefix(appBundle + "/") { shouldInclude = false break } } if shouldInclude { filteredFiles.append(file) } } return filteredFiles } private func getBomFilesByExistence(for package: PackageInfo) -> (existing: [String], deleted: [String]) { let systemDirectoriesToFilter = [ "/Applications", "/Library", "/Library/Application Support", "/Library/Frameworks", "/Library/LaunchAgents", "/Library/LaunchDaemons", "/Library/PreferencePanes", "/Library/PrivilegedHelperTools", "/Library/QuickLook", "/Library/Receipts", "/Library/StartupItems", "/System", "/System/Library", "/root", "/", "/usr", "/usr/bin", "/usr/lib", "/usr/libexec", "/usr/local", "/usr/local/bin", "/usr/local/lib", "/usr/local/share", "/usr/sbin", "/usr/share", "/var", "/var/db", "/var/log", "/private", "/private/etc", "/private/tmp", "/private/var", "/etc", "/tmp", "/opt", "/opt/local", "/opt/local/bin", "/opt/local/lib", // Additional high-risk directories that should NEVER be deleted as parent directories "/Library/Extensions", // Kernel extensions "/Library/Audio", "/Library/Audio/Plug-Ins", "/Library/Audio/Plug-Ins/HAL", // Audio plugins "/Library/Audio/Plug-Ins/Components", "/Library/Audio/Plug-Ins/VST", "/Library/Audio/Plug-Ins/VST3", "/Library/Preferences", // System preferences "/Library/LaunchAgents", "/Library/LaunchDaemons", // Launch items "/Library/Components" // System components ] var existingFiles: [String] = [] var deletedFiles: [String] = [] // Create a list that includes the install location (if not root) plus all BOM files var filesToProcess = package.bomFiles // Add install location to the list if it's meaningful and not a system directory if !package.installLocation.isEmpty && package.installLocation != "/" { var installLocationPath = package.installLocation if !installLocationPath.hasPrefix("/") { installLocationPath = "/" + installLocationPath } if !systemDirectoriesToFilter.contains(installLocationPath) && !filesToProcess.contains(installLocationPath) { filesToProcess.append(installLocationPath) } } for file in filesToProcess { // Filter out system directories themselves (but NOT their children) // e.g., filter "/Library/Extensions" but allow "/Library/Extensions/Foo.kext" if systemDirectoriesToFilter.contains(file) { continue } // Check if file exists if FileManager.default.fileExists(atPath: file) { existingFiles.append(file) } else { deletedFiles.append(file) } } // Remove redundant child paths from both existing and deleted files let filteredExistingFiles = removeRedundantChildPaths(existingFiles) let filteredDeletedFiles = removeRedundantChildPaths(deletedFiles) return (filteredExistingFiles.sorted(), filteredDeletedFiles.sorted()) } // Helper function to remove redundant child paths for known bundle types // This collapses bundle internals (e.g., Foo.app/Contents/...) into just the bundle (Foo.app) // SAFETY: Only collapses recognized bundle extensions, never arbitrary directories private func removeRedundantChildPaths(_ paths: [String]) -> [String] { // Comprehensive list of macOS bundle types that should be collapsed let bundleExtensions = [ // Applications & System ".app", ".appex", ".xpc", // Drivers & Kernel ".kext", ".driver", // Plugins & Components ".plugin", ".bundle", ".component", ".vst", ".vst3", ".au", // Frameworks & Libraries ".framework", ".dylib", // System Services ".service", ".prefPane", ".menu", ".qlgenerator", ".saver", ".mdimporter", ".action", ".workflow", // Development ".xcodeproj", ".xcworkspace", ".playground", ".xcframework", ".dSYM", // Miscellaneous ".pkg", ".lpkg", ".clr", ".slideSaver" ] let sortedPaths = paths.sorted() var result: [String] = [] for path in sortedPaths { var isInsideBundle = false // Check if this path is inside a known bundle type for existingPath in result { // Only treat as redundant if parent is a recognized bundle let isBundle = bundleExtensions.contains { existingPath.hasSuffix($0) } if isBundle && path.hasPrefix(existingPath + "/") { isInsideBundle = true break } } if !isInsideBundle { result.append(path) } } return result } struct BomFileRowView: View { let file: String let exists: Bool let index: Int let colorScheme: ColorScheme let onView: () -> Void let onRemove: () -> Void var body: some View { HStack { Image(systemName: exists ? "doc.text" : "doc.text.fill") .font(.caption) .foregroundStyle(exists ? ThemeColors.shared(for: colorScheme).secondaryText : ThemeColors.shared(for: colorScheme).secondaryText.opacity(0.4)) Text(file) .font(.caption) .foregroundStyle(exists ? ThemeColors.shared(for: colorScheme).primaryText : ThemeColors.shared(for: colorScheme).secondaryText.opacity(0.5)) .lineLimit(1) .truncationMode(.middle) .strikethrough(exists ? false : true) Spacer() if exists { HStack(spacing: 10) { Button("View") { onView() } .buttonStyle(.borderless) .controlSize(.mini) .foregroundStyle(.blue) .help("Show file in Finder") Divider().frame(height: 10) Button("Remove") { onRemove() } .buttonStyle(.borderless) .controlSize(.mini) .foregroundStyle(.red) .help("Delete this file") } } else { Text("DELETED") .font(.caption2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText.opacity(0.4)) .fontWeight(.medium) } } .padding(.vertical, 2) .background(index % 2 == 0 ? Color.clear : ThemeColors.shared(for: colorScheme).secondaryText.opacity(0.05)) .opacity(exists ? 1.0 : 0.6) } } // MARK: - Package Uninstall Sheet struct PackageUninstallSheet: View { let package: PackageInfo let files: [String] @Binding var selectedFiles: Set let onConfirm: () -> Void let onCancel: () -> Void @State private var searchText = "" @Environment(\.colorScheme) var colorScheme private var filteredFiles: [String] { if searchText.isEmpty { return files } return files.filter { $0.localizedCaseInsensitiveContains(searchText) } } var body: some View { StandardSheetView( title: "Uninstall Package", width: 600, height: 500, onClose: onCancel ) { // Content VStack(spacing: 0) { // Subtitle with package name VStack(spacing: 8) { Text(package.displayName) .font(.headline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Text("\(selectedFiles.count) of \(files.count) file\(files.count == 1 ? "" : "s") selected") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } // Search bar HStack { Image(systemName: "magnifyingglass") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) TextField("Filter files...", text: $searchText) .textFieldStyle(.plain) if !searchText.isEmpty { Button { searchText = "" } label: { Image(systemName: "xmark.circle.fill") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .buttonStyle(.plain) } } .padding(.top, 12) Divider() .padding(.top, 12) // File list or loading state if files.isEmpty { // Loading state VStack(spacing: 12) { ProgressView() .scaleEffect(1.5) Text("Loading package files...") .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .frame(maxWidth: .infinity, maxHeight: 400) .frame(minHeight: 200) } else { ScrollView { LazyVStack(alignment: .leading, spacing: 2) { ForEach(filteredFiles, id: \.self) { file in HStack(spacing: 8) { Button { toggleSelection(file) } label: { Image(systemName: selectedFiles.contains(file) ? "checkmark.square.fill" : "square") .foregroundStyle(selectedFiles.contains(file) ? .blue : ThemeColors.shared(for: colorScheme).secondaryText) } .buttonStyle(.plain) Text(file) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .lineLimit(1) .truncationMode(.middle) Spacer() } .padding(.horizontal) .padding(.vertical, 4) .background(filteredFiles.firstIndex(of: file).map { $0 % 2 == 0 } == true ? Color.clear : ThemeColors.shared(for: colorScheme).secondaryText.opacity(0.05)) } } .padding(.vertical, 8) } .frame(maxHeight: 400) } } } selectionControls: { Button(selectedFiles.count == files.count ? "Deselect All" : "Select All") { if selectedFiles.count == files.count { selectedFiles.removeAll() } else { selectedFiles = Set(files) } } .buttonStyle(.borderless) .disabled(files.isEmpty) } actionButtons: { Button("Cancel") { onCancel() } .keyboardShortcut(.cancelAction) Button("Move to Trash") { onConfirm() } .keyboardShortcut(.defaultAction) .disabled(selectedFiles.isEmpty || files.isEmpty) } } private func toggleSelection(_ file: String) { if selectedFiles.contains(file) { selectedFiles.remove(file) } else { selectedFiles.insert(file) } } } ================================================ FILE: Pearcleaner/Views/PluginsView.swift ================================================ // // PluginsView.swift // Pearcleaner // // Created by Alin Lupascu on 09/29/25. // import SwiftUI import AlinFoundation enum PluginSortOption: String, CaseIterable { case name = "Name" case category = "Category" case size = "Size" case dateModified = "Date Modified" var displayName: String { return self.rawValue } var systemImage: String { switch self { case .name: return "list.bullet" case .category: return "folder.fill" case .size: return "arrow.up.arrow.down" case .dateModified: return "clock" } } } struct PluginInfo: Identifiable, Hashable, Equatable { let name: String let path: String // Use path as stable ID for proper SwiftUI row reuse var id: String { path } let category: String let isDirectory: Bool var size: Int64? // Optional - calculated lazily on row appear let dateModified: Date let bundleId: String? let customIcon: NSImage? var displayName: String { return name } static func == (lhs: PluginInfo, rhs: PluginInfo) -> Bool { return lhs.id == rhs.id && lhs.name == rhs.name && lhs.path == rhs.path && lhs.category == rhs.category && lhs.bundleId == rhs.bundleId } func hash(into hasher: inout Hasher) { hasher.combine(id) hasher.combine(name) hasher.combine(path) hasher.combine(category) hasher.combine(bundleId) } } struct PluginsView: View { @EnvironmentObject var appState: AppState @EnvironmentObject var locations: Locations @Environment(\.colorScheme) var colorScheme @ObservedObject private var consoleManager = GlobalConsoleManager.shared @State private var allPlugins: [PluginInfo] = [] @State private var isLoading: Bool = false @State private var lastRefreshDate: Date? @State private var searchText: String = "" @State private var sortOption: PluginSortOption = .name @State private var selectedPlugins: Set = [] // Changed to String since ID is now path @State private var selectedPluginPaths: [String] = [] @State private var collapsedCategories: Set = [] @State private var cachedSizes: [String: Int64] = [:] // Cache for lazily calculated sizes, keyed by plugin path @AppStorage("settings.interface.scrollIndicators") private var scrollIndicators: Bool = false @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true private var filteredPlugins: [PluginInfo] { var filteredList = allPlugins if !searchText.isEmpty { filteredList = filteredList.filter { plugin in plugin.name.localizedCaseInsensitiveContains(searchText) || plugin.path.localizedCaseInsensitiveContains(searchText) || plugin.category.localizedCaseInsensitiveContains(searchText) } } switch sortOption { case .name: filteredList = filteredList.sorted { first, second in return first.name.localizedCaseInsensitiveCompare(second.name) == .orderedAscending } case .category: filteredList = filteredList.sorted { first, second in if first.category != second.category { return first.category.localizedCaseInsensitiveCompare(second.category) == .orderedAscending } return first.name.localizedCaseInsensitiveCompare(second.name) == .orderedAscending } case .size: filteredList = filteredList.sorted { ($0.size ?? 0) > ($1.size ?? 0) } case .dateModified: filteredList = filteredList.sorted { $0.dateModified > $1.dateModified } } return filteredList } private var groupedPlugins: [String: [PluginInfo]] { Dictionary(grouping: filteredPlugins) { $0.category } } var body: some View { VStack(alignment: .leading, spacing: 0) { // Search bar HStack { Image(systemName: "magnifyingglass") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) TextField("Search...", text: $searchText) .textFieldStyle(.plain) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) if !searchText.isEmpty { Button { searchText = "" } label: { Image(systemName: "xmark.circle.fill") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .buttonStyle(.plain) } } .padding(.horizontal, 12) .padding(.vertical, 8) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) .controlGroup(Capsule(style: .continuous), level: .primary) .padding(.top, 5) if isLoading && allPlugins.isEmpty { VStack(alignment: .center, spacing: 10) { Spacer() ProgressView() .scaleEffect(1.5) Text("Loading plugins...") .font(.title2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() } .frame(maxWidth: .infinity) } else if allPlugins.isEmpty && !isLoading { VStack(alignment: .center) { Spacer() Text("No plugins found") .font(.title2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() } .frame(maxWidth: .infinity) } else { // Stats header HStack { Text("\(filteredPlugins.count) plugin\(filteredPlugins.count == 1 ? "" : "s")") .font(.caption) .monospacedDigit() .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) if isLoading { Text("Loading...") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } Spacer() if let lastRefresh = lastRefreshDate { TimelineView(.periodic(from: lastRefresh, by: 1.0)) { _ in Text("Updated \(formatRelativeTime(lastRefresh))") .font(.caption) .monospacedDigit() .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } } .padding(.vertical) ScrollView { LazyVStack(spacing: 12) { ForEach(Array(groupedPlugins.keys.sorted()), id: \.self) { category in if let categoryPlugins = groupedPlugins[category] { GroupBox { if !collapsedCategories.contains(category) { LazyVStack(spacing: 8) { ForEach(categoryPlugins, id: \.id) { plugin in if category == "Audio" { AudioPluginRowView( plugin: plugin, isSelected: selectedPlugins.contains(plugin.id), sortOption: sortOption, cachedSize: cachedSizes[plugin.path], onSizeCalculated: { size in cachedSizes[plugin.path] = size } ) { removePlugin(plugin) } onRefresh: { refreshPlugins() } onToggleSelection: { if selectedPlugins.contains(plugin.id) { selectedPlugins.remove(plugin.id) selectedPluginPaths.removeAll { $0 == plugin.path } } else { selectedPlugins.insert(plugin.id) selectedPluginPaths.append(plugin.path) } } } else { PluginRowView( plugin: plugin, isSelected: selectedPlugins.contains(plugin.id), sortOption: sortOption, cachedSize: cachedSizes[plugin.path], onSizeCalculated: { size in cachedSizes[plugin.path] = size } ) { removePlugin(plugin) } onRefresh: { refreshPlugins() } onToggleSelection: { if selectedPlugins.contains(plugin.id) { selectedPlugins.remove(plugin.id) selectedPluginPaths.removeAll { $0 == plugin.path } } else { selectedPlugins.insert(plugin.id) selectedPluginPaths.append(plugin.path) } } } } } } } .groupBoxStyle(.collapsible( icon: iconForCategory(category), title: category, count: categoryPlugins.count, isCollapsed: collapsedCategories.contains(category), onToggle: { toggleCategoryCollapse(for: category) } )) } } } } .scrollIndicators(scrollIndicators ? .automatic : .never) } } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(.horizontal, 20) .onAppear { // Start loading plugins immediately but non-blocking if allPlugins.isEmpty { Task { await refreshPluginsAsync() } } } .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("PluginsViewShouldRefresh"))) { _ in // Refresh plugins Task { await refreshPluginsAsync() } } .safeAreaInset(edge: .bottom) { if !selectedPlugins.isEmpty { HStack { Spacer() HStack(spacing: 10) { Button(selectedPlugins.count == filteredPlugins.count ? "Deselect All" : "Select All") { if selectedPlugins.count == filteredPlugins.count { selectedPlugins.removeAll() selectedPluginPaths.removeAll() } else { selectAllItems() } } .buttonStyle(ControlGroupButtonStyle( foregroundColor: ThemeColors.shared(for: colorScheme).accent, shape: Capsule(style: .continuous), level: .primary, skipControlGroup: true )) Divider().frame(height: 10) Button("Delete \(selectedPlugins.count) Selected") { deleteSelectedItems() } .buttonStyle(ControlGroupButtonStyle( foregroundColor: ThemeColors.shared(for: colorScheme).accent, shape: Capsule(style: .continuous), level: .primary, skipControlGroup: true )) } .controlGroup(Capsule(style: .continuous), level: .primary) Spacer() } .padding([.horizontal, .bottom]) } } .toolbarBackground(.hidden, for: .windowToolbar) .toolbar { TahoeToolbarItem(placement: .navigation) { HStack { VStack(alignment: .leading) { Text("Plugin Manager") .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .font(.title2) .fontWeight(.bold) Text("Manage third-party plugins and extensions") .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } } ToolbarItem { Spacer() } TahoeToolbarItem(isGroup: true) { Button { withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) { consoleManager.showConsole.toggle() } } label: { Label("Console", systemImage: consoleManager.showConsole ? "terminal.fill" : "terminal") } .help("Toggle console output") Menu { ForEach(PluginSortOption.allCases, id: \.self) { option in Button { sortOption = option } label: { Label(option.displayName, systemImage: option.systemImage) } } } label: { Label(sortOption.displayName, systemImage: sortOption.systemImage) } .labelStyle(.titleAndIcon) .menuIndicator(.hidden) Button { refreshPlugins() } label: { Label("Refresh", systemImage: "arrow.counterclockwise") } .disabled(isLoading) } } } // MARK: - Helper Functions private func refreshPlugins() { Task { await refreshPluginsAsync() } } private func refreshPluginsAsync() async { GlobalConsoleManager.shared.appendOutput("Refreshing plugins...\n", source: CurrentPage.plugins.title) await MainActor.run { isLoading = true allPlugins = [] selectedPlugins = [] selectedPluginPaths = [] cachedSizes = [:] // Clear size cache on refresh } // Load plugins with incremental updates await loadPluginsIncremental() await MainActor.run { self.lastRefreshDate = Date() self.isLoading = false GlobalConsoleManager.shared.appendOutput("✓ Loaded \(allPlugins.count) plugins\n", source: CurrentPage.plugins.title) } } private func loadPluginsIncremental() async { let fileManager = FileManager.default let pluginCategories = locations.plugins.subcategories // Process categories concurrently with incremental UI updates await withTaskGroup(of: Void.self) { group in // Add tasks for each category for (category, paths) in pluginCategories { group.addTask { await self.processCategory(category: category, paths: paths, fileManager: fileManager) } } // Wait for all categories to complete (no results to collect - processCategory updates UI directly) await group.waitForAll() } } private func processCategory(category: String, paths: [String], fileManager: FileManager) async { var batchBuffer: [PluginInfo] = [] let batchSize = 20 // Update UI every 20 plugins for smooth incremental loading for path in paths { // Check if path exists before trying to read it guard fileManager.fileExists(atPath: path) else { continue } do { let contents = try fileManager.contentsOfDirectory( at: URL(fileURLWithPath: path), includingPropertiesForKeys: [.isDirectoryKey, .fileSizeKey, .contentModificationDateKey], options: [.skipsHiddenFiles] ) for itemURL in contents { do { let resourceValues = try itemURL.resourceValues( forKeys: [.isDirectoryKey, .fileSizeKey, .contentModificationDateKey] ) let isDirectory = resourceValues.isDirectory ?? false // Don't calculate size during initial load - will be lazily calculated on row appear let size: Int64? = nil let dateModified = resourceValues.contentModificationDate ?? Date() let name = itemURL.lastPathComponent // Filter out system files and hidden files if !name.hasPrefix(".") && !name.hasPrefix("~") { // Check if file matches the expected types for this category if shouldIncludeFile(name: name, isDirectory: isDirectory, category: category) { // Bundle ID will be loaded lazily when needed for search let bundleId: String? = nil let plugin = PluginInfo( name: name, path: itemURL.path, category: category, isDirectory: isDirectory, size: size, dateModified: dateModified, bundleId: bundleId, customIcon: nil ) batchBuffer.append(plugin) // Update UI every batchSize plugins for incremental loading if batchBuffer.count >= batchSize { let pluginsToAdd = batchBuffer batchBuffer = [] await MainActor.run { self.allPlugins.append(contentsOf: pluginsToAdd) } } } } } catch { // Skip items that can't be read continue } } } catch { // Skip directories that don't exist or can't be read continue } } // Flush any remaining plugins in the batch buffer if !batchBuffer.isEmpty { await MainActor.run { self.allPlugins.append(contentsOf: batchBuffer) } } // #if DEBUG // // Duplicate ZoomAudioDevice.driver 599 more times for performance testing // if let zoomPlugin = self.allPlugins.first(where: { $0.name == "ZoomAudioDevice.driver" && $0.category == category }) { // var debugPlugins: [PluginInfo] = [] // for i in 1...599 { // let duplicatedPlugin = PluginInfo( // name: "\(zoomPlugin.name) (Copy \(i))", // path: "\(zoomPlugin.path)_copy\(i)", // Fake path // category: zoomPlugin.category, // isDirectory: zoomPlugin.isDirectory, // size: zoomPlugin.size, // dateModified: zoomPlugin.dateModified, // bundleId: zoomPlugin.bundleId.map { "\($0).copy\(i)" }, // customIcon: zoomPlugin.customIcon // ) // debugPlugins.append(duplicatedPlugin) // } // // // Add debug plugins in batches for smooth UI // let debugBatches = debugPlugins.chunked(into: batchSize) // for batch in debugBatches { // await MainActor.run { // self.allPlugins.append(contentsOf: batch) // } // } // } // #endif } private func removePlugin(_ plugin: PluginInfo) { Task { await performPluginRemoval(plugin) } } private func performPluginRemoval(_ plugin: PluginInfo) async { GlobalConsoleManager.shared.appendOutput("Starting deletion of \(plugin.name)...\n", source: CurrentPage.plugins.title) let success = await withCheckedContinuation { continuation in DispatchQueue.global(qos: .userInitiated).async { let pluginURL = URL(fileURLWithPath: plugin.path) let bundleName = "\(plugin.category) Plugin - \(plugin.name)" let success = FileManagerUndo.shared.deleteFiles(at: [pluginURL], bundleName: bundleName) continuation.resume(returning: success) } } await MainActor.run { if success { GlobalConsoleManager.shared.appendOutput("✓ Completed deletion of \(plugin.name)\n", source: CurrentPage.plugins.title) // Remove the plugin from the local arrays allPlugins.removeAll { $0.id == plugin.id } selectedPlugins.remove(plugin.id) selectedPluginPaths.removeAll { $0 == plugin.path } } else { showCustomAlert( title: "Deletion Failed", message: "Failed to delete plugin '\(plugin.name)'. The plugin may require additional permissions or may not exist.", style: .critical ) } } } private func selectAllItems() { selectedPlugins = Set(filteredPlugins.map { $0.id }) selectedPluginPaths = filteredPlugins.map { $0.path } } private func deleteSelectedItems() { let pluginsToDelete = allPlugins.filter { selectedPlugins.contains($0.id) } let urlsToDelete = pluginsToDelete.map { URL(fileURLWithPath: $0.path) } Task { GlobalConsoleManager.shared.appendOutput("Starting deletion of \(pluginsToDelete.count) selected plugin(s)...\n", source: CurrentPage.plugins.title) let success = await withCheckedContinuation { continuation in DispatchQueue.global(qos: .userInitiated).async { // Create descriptive bundle name for multiple plugins let categories = Set(pluginsToDelete.map { $0.category }) let bundleName: String if categories.count == 1, let category = categories.first { bundleName = "\(category) Plugins (\(pluginsToDelete.count) items)" } else { bundleName = "Mixed Plugins (\(pluginsToDelete.count) items)" } let success = FileManagerUndo.shared.deleteFiles(at: urlsToDelete, bundleName: bundleName) continuation.resume(returning: success) } } await MainActor.run { if success { GlobalConsoleManager.shared.appendOutput("✓ Completed deletion of \(pluginsToDelete.count) plugin(s)\n", source: CurrentPage.plugins.title) // Remove all deleted plugins from the local arrays let deletedIds = Set(pluginsToDelete.map { $0.id }) allPlugins.removeAll { deletedIds.contains($0.id) } selectedPlugins.removeAll() selectedPluginPaths.removeAll() } else { showCustomAlert( title: "Deletion Failed", message: "Failed to delete some selected plugins. They may require additional permissions or may not exist.", style: .critical ) } } } } private func toggleCategoryCollapse(for category: String) { if collapsedCategories.contains(category) { collapsedCategories.remove(category) } else { collapsedCategories.insert(category) } } private func iconForCategory(_ category: String) -> String? { switch category { case "Audio": return "music.note" case "PreferencePanes": return "gearshape.2" case "QuickLook": return "eye" case "Screen Savers": return "photo.on.rectangle.angled" case "Internet Plug-Ins": return "network" case "Core Image": return "camera.filters" case "ColorPickers": return "paintpalette" case "Fonts": return "textformat" case "Dictionaries": return "book.closed" case "Automator": return "gearshape.arrow.triangle.2.circlepath" case "Safari Extensions": return "safari" case "Motion Templates": return "film" case "Spotlight": return "magnifyingglass" case "Services": return "gear" case "Address Book": return "person.crop.rectangle" case "Contextual Menu": return "contextualmenu.and.cursorarrow" case "Input Methods": return "keyboard" case "Widgets": return "square.grid.2x2" default: return nil } } private func extractBundleId(from path: String, category: String, isDirectory: Bool) async -> String? { // Only process Audio category .driver bundles guard category == "Audio", isDirectory, path.lowercased().hasSuffix(".driver") else { return nil } let infoPlistPath = path + "/Contents/Info.plist" guard FileManager.default.fileExists(atPath: infoPlistPath) else { return nil } // Read Info.plist if let plistData = NSDictionary(contentsOfFile: infoPlistPath) { // Extract bundle identifier return plistData["CFBundleIdentifier"] as? String } return nil } private func shouldIncludeFile(name: String, isDirectory: Bool, category: String) -> Bool { let lowercaseName = name.lowercased() switch category { case "Audio": // Audio category includes all files and directories return true case "PreferencePanes": // System Preference Panes (.prefPane) return lowercaseName.hasSuffix(".prefpane") case "QuickLook": // QuickLook Generators (.qlgenerator) return lowercaseName.hasSuffix(".qlgenerator") case "Screen Savers": // Screen Savers (.saver) return lowercaseName.hasSuffix(".saver") case "Internet Plug-Ins": // Browser plugins (.plugin, .webplugin) return lowercaseName.hasSuffix(".plugin") || lowercaseName.hasSuffix(".webplugin") case "Core Image": // Core Image filters (.plugin) return lowercaseName.hasSuffix(".plugin") case "ColorPickers": // Color Picker plugins (.colorPicker) return lowercaseName.hasSuffix(".colorpicker") case "Fonts": // Font files (.ttf, .otf, .dfont, .ttc) return lowercaseName.hasSuffix(".ttf") || lowercaseName.hasSuffix(".otf") || lowercaseName.hasSuffix(".dfont") || lowercaseName.hasSuffix(".ttc") case "Dictionaries": // Dictionary files (.dictionary) return lowercaseName.hasSuffix(".dictionary") case "Automator": // Automator Actions (.action, .workflow) return lowercaseName.hasSuffix(".action") || lowercaseName.hasSuffix(".workflow") case "Safari Extensions": // Safari Extensions (.safariextz, .appex) return lowercaseName.hasSuffix(".safariextz") || lowercaseName.hasSuffix(".appex") case "Motion Templates": // Final Cut Pro and Motion templates (various extensions, check for directories too) return isDirectory || lowercaseName.contains("template") || lowercaseName.hasSuffix(".motn") case "Spotlight": // Spotlight importers (.mdimporter) return lowercaseName.hasSuffix(".mdimporter") case "Services": // System Services (.service) return lowercaseName.hasSuffix(".service") case "Address Book": // Address Book plugins (usually directories or .plugin files) return isDirectory || lowercaseName.hasSuffix(".plugin") case "Contextual Menu": // Context menu plugins (various formats) return isDirectory || lowercaseName.hasSuffix(".plugin") || lowercaseName.hasSuffix(".bundle") case "Input Methods": // Input method editors (usually .app bundles or directories) return isDirectory || lowercaseName.hasSuffix(".app") || lowercaseName.hasSuffix(".bundle") case "Widgets": // Dashboard and notification widgets (.wdgt, .appex) return lowercaseName.hasSuffix(".wdgt") || lowercaseName.hasSuffix(".appex") default: // For unknown categories, include all files return true } } } // MARK: - Plugin Category Section struct PluginCategorySection: View { let category: String let plugins: [PluginInfo] let isCollapsed: Bool @Binding var selectedPlugins: Set // Changed to String since ID is now path @Binding var selectedPluginPaths: [String] @Binding var cachedSizes: [String: Int64] // Add binding for cached sizes let sortOption: PluginSortOption let onRemove: (PluginInfo) -> Void let onRefresh: () -> Void let onToggleCategoryCollapse: () -> Void @Environment(\.colorScheme) var colorScheme @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true var body: some View { VStack(alignment: .leading, spacing: 8) { // Category header Button(action: onToggleCategoryCollapse) { HStack { Image(systemName: isCollapsed ? "chevron.right" : "chevron.down") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .frame(width: 10) Text(category) .font(.headline) .fontWeight(.semibold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Text(verbatim: "(\(plugins.count))") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() } .padding(.horizontal) .padding(.vertical, 4) } .buttonStyle(.plain) .contentShape(Rectangle()) // Plugin rows if !isCollapsed { ForEach(plugins, id: \.id) { plugin in if category == "Audio" { AudioPluginRowView( plugin: plugin, isSelected: selectedPlugins.contains(plugin.id), sortOption: sortOption, cachedSize: cachedSizes[plugin.path], onSizeCalculated: { size in cachedSizes[plugin.path] = size } ) { onRemove(plugin) } onRefresh: { onRefresh() } onToggleSelection: { toggleSelection(for: plugin) } } else { PluginRowView( plugin: plugin, isSelected: selectedPlugins.contains(plugin.id), sortOption: sortOption, cachedSize: cachedSizes[plugin.path], onSizeCalculated: { size in cachedSizes[plugin.path] = size } ) { onRemove(plugin) } onRefresh: { onRefresh() } onToggleSelection: { toggleSelection(for: plugin) } } } .transition(plugins.count > 50 ? .identity : .opacity) } } } private func toggleSelection(for plugin: PluginInfo) { if selectedPlugins.contains(plugin.id) { selectedPlugins.remove(plugin.id) selectedPluginPaths.removeAll { $0 == plugin.path } } else { selectedPlugins.insert(plugin.id) selectedPluginPaths.append(plugin.path) } } } // MARK: - Plugin Row View struct PluginRowView: View { let plugin: PluginInfo let isSelected: Bool let sortOption: PluginSortOption let cachedSize: Int64? // Size from cache, nil if not yet calculated let onSizeCalculated: (Int64) -> Void // Callback to update cache let onRemove: () -> Void let onRefresh: () -> Void let onToggleSelection: () -> Void @Environment(\.colorScheme) var colorScheme @State private var isPerformingAction = false @State private var isHovered = false var body: some View { HStack(spacing: 15) { // Selection checkbox - OUTSIDE the background Button(action: onToggleSelection) { EmptyView() } .buttonStyle(CircleCheckboxButtonStyle(isSelected: isSelected)) // Content with background HStack(alignment: .center, spacing: 12) { // Plugin icon and type indicator VStack(spacing: 4) { ZStack { if plugin.customIcon == nil { Circle() .fill(pluginColor.opacity(0.2)) .frame(width: 32, height: 32) } if let customIcon = plugin.customIcon { Image(nsImage: customIcon) .resizable() .scaledToFit() .frame(width: 32, height: 32) } else { Image(systemName: pluginIcon) .font(.system(size: 14, weight: .medium)) .foregroundStyle(pluginColor) } } } // Plugin details VStack(alignment: .leading, spacing: 4) { Text(plugin.name) .font(.headline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .lineLimit(1) Text("Path: \(plugin.path)") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .lineLimit(1) .truncationMode(.middle) HStack { Text("Category: \(plugin.category)") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text(verbatim: "•") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text("Size: \(formatFileSize(cachedSize ?? plugin.size ?? 0))") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() } // Bundle ID for .driver bundles if let bundleId = plugin.bundleId, !bundleId.isEmpty { HStack { Text("Bundle ID:") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text(bundleId) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .textSelection(.enabled) Spacer() } } } Spacer() // Action buttons VStack(spacing: 6) { HStack(spacing: 10) { Button("View") { openInFinder(plugin.path) } .buttonStyle(.borderless) .controlSize(.mini) .foregroundStyle(.blue) .disabled(isPerformingAction) .help("Show in Finder") Divider().frame(height: 10) Button("Delete") { onRemove() } .buttonStyle(.borderless) .controlSize(.mini) .foregroundStyle(.red) .disabled(isPerformingAction) .help("Delete plugin") } if isPerformingAction { ProgressView() .scaleEffect(0.7) } } } .padding() .background( RoundedRectangle(cornerRadius: 8) .fill(isHovered ? ThemeColors.shared(for: colorScheme).secondaryBG.opacity(0.8) : ThemeColors.shared(for: colorScheme).secondaryBG ) ) .onHover { hovering in withAnimation(.easeInOut(duration: 0.2)) { isHovered = hovering } } } .onAppear { // Lazy size calculation - only compute when row appears if cachedSize == nil { Task.detached(priority: .utility) { let url = URL(fileURLWithPath: plugin.path) let calculatedSize: Int64 if plugin.isDirectory { calculatedSize = totalSizeOnDisk(for: url) } else { calculatedSize = (try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize.map { Int64($0) }) ?? 0 } await MainActor.run { onSizeCalculated(calculatedSize) } } } } } private var pluginColor: Color { switch plugin.category { case "Audio": return .purple case "PreferencePanes": return .blue case "QuickLook": return .green case "Screen Savers": return .orange case "Internet Plug-Ins": return .red case "ColorPickers": return .pink case "Fonts": return .brown case "Safari Extensions": return .cyan case "Widgets": return .teal default: return .gray } } private var pluginIcon: String { switch plugin.category { case "Audio": return "waveform" case "PreferencePanes": return "gearshape" case "QuickLook": return "magnifyingglass" case "Screen Savers": return "display" case "Internet Plug-Ins": return "globe" case "ColorPickers": return "paintpalette" case "Fonts": return "textformat" case "Safari Extensions": return "safari" case "Widgets": return "square.grid.3x3" default: return "puzzlepiece" } } private func formatFileSize(_ size: Int64) -> String { let formatter = ByteCountFormatter() formatter.allowsNonnumericFormatting = false formatter.countStyle = .file return formatter.string(fromByteCount: size) } private func formatDate(_ date: Date) -> String { let formatter = DateFormatter() formatter.dateStyle = .medium formatter.timeStyle = .short return formatter.string(from: date) } private func openInFinder(_ path: String) { let url = URL(fileURLWithPath: path) NSWorkspace.shared.activateFileViewerSelecting([url]) } } // MARK: - Audio Plugin Row View (with Related Files Search) struct AudioPluginRowView: View { let plugin: PluginInfo let isSelected: Bool let sortOption: PluginSortOption let cachedSize: Int64? // Size from cache, nil if not yet calculated let onSizeCalculated: (Int64) -> Void // Callback to update cache let onRemove: () -> Void let onRefresh: () -> Void let onToggleSelection: () -> Void @EnvironmentObject var locations: Locations @Environment(\.colorScheme) var colorScheme @State private var isPerformingAction = false @State private var isHovered = false @State private var isExpanded = false @State private var relatedFiles: [FileSearchResult] = [] @State private var isSearching = false @State private var searchEngine: FileSearchEngine? @State private var selectedRelatedFiles: Set = [] var body: some View { HStack(spacing: 15) { // Selection checkbox - OUTSIDE the background Button(action: { // When selecting/deselecting plugin, also select/deselect all related files if !isSelected { // Plugin is about to be selected, select all related files selectedRelatedFiles = Set(relatedFiles.map { $0.id }) } else { // Plugin is about to be deselected, deselect all related files selectedRelatedFiles.removeAll() } onToggleSelection() }) { EmptyView() } .buttonStyle(CircleCheckboxButtonStyle(isSelected: isSelected)) // Content with background VStack(alignment: .leading, spacing: 0) { // Main plugin row HStack(alignment: .center, spacing: 12) { // Plugin icon and type indicator VStack(spacing: 4) { ZStack { if plugin.customIcon == nil { Circle() .fill(pluginColor.opacity(0.2)) .frame(width: 32, height: 32) } if let customIcon = plugin.customIcon { Image(nsImage: customIcon) .resizable() .scaledToFit() .frame(width: 32, height: 32) } else { Image(systemName: pluginIcon) .font(.system(size: 14, weight: .medium)) .foregroundStyle(pluginColor) } } } // Plugin details VStack(alignment: .leading, spacing: 4) { Text(plugin.name) .font(.headline) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .lineLimit(1) Text("Path: \(plugin.path)") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .lineLimit(1) .truncationMode(.middle) HStack { Text("Category: \(plugin.category)") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text(verbatim: "•") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text("Size: \(formatFileSize(cachedSize ?? plugin.size ?? 0))") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() } // Bundle ID for .driver bundles if let bundleId = plugin.bundleId, !bundleId.isEmpty { HStack { Text("Bundle ID:") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text(bundleId) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .textSelection(.enabled) Spacer() } } } Spacer() // Action buttons if isPerformingAction { ProgressView() .scaleEffect(0.7) } Button(isExpanded ? "Close" : "Search") { withAnimation(.easeInOut(duration: 0.2)) { isExpanded.toggle() } } .buttonStyle(.borderless) .controlSize(.mini) .foregroundStyle(.blue) .disabled(isPerformingAction) .help(isExpanded ? "Close related files search" : "Search for related files") Button("View") { openInFinder(plugin.path) } .buttonStyle(.borderless) .controlSize(.mini) .foregroundStyle(.blue) .disabled(isPerformingAction) .help("Show in Finder") Button("Delete") { deletePluginAndRelatedFiles() } .buttonStyle(.borderless) .controlSize(.mini) .foregroundStyle(.red) .disabled(isPerformingAction) .help("Delete plugin and selected related files") } .padding() // Expandable related files section if isExpanded { VStack(alignment: .leading, spacing: 8) { Divider() .padding(.horizontal) if isSearching { HStack(spacing: 8) { ProgressView() .scaleEffect(0.8) Text("Searching for related files...") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .padding(.horizontal) .padding(.vertical, 8) } else if relatedFiles.isEmpty { Text("No related files found") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .padding(.horizontal) .padding(.vertical, 8) } else { VStack(alignment: .leading, spacing: 4) { Text("\(relatedFiles.count) related file\(relatedFiles.count == 1 ? "" : "s") found") .font(.caption) .fontWeight(.semibold) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .padding(.horizontal) ForEach(relatedFiles, id: \.id) { result in HStack(spacing: 8) { // Checkbox for related file Button(action: { toggleRelatedFileSelection(result) }) { Image(systemName: selectedRelatedFiles.contains(result.id) ? "checkmark.circle.fill" : "circle") .foregroundStyle(selectedRelatedFiles.contains(result.id) ? ThemeColors.shared(for: colorScheme).accent : ThemeColors.shared(for: colorScheme).secondaryText) .font(.caption) } .buttonStyle(.plain) Image(systemName: result.isDirectory ? "folder.fill" : "doc.fill") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .frame(width: 12) VStack(alignment: .leading, spacing: 2) { Text(result.name) .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .lineLimit(1) Text(result.url.path) .font(.system(size: 10)) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .lineLimit(1) .truncationMode(.middle) } Spacer() Text(formatFileSize(result.size)) .font(.system(size: 10)) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Button("View") { NSWorkspace.shared.activateFileViewerSelecting([result.url]) } .buttonStyle(.borderless) .controlSize(.mini) .foregroundStyle(.blue) Button("Delete") { deleteRelatedFile(result) } .buttonStyle(.borderless) .controlSize(.mini) .foregroundStyle(.red) .help("Delete this file") } .padding(.horizontal) .padding(.vertical, 4) } } .padding(.vertical, 4) } } .transition(.opacity) } } .background( RoundedRectangle(cornerRadius: 8) .fill(isHovered ? ThemeColors.shared(for: colorScheme).secondaryBG.opacity(0.8) : ThemeColors.shared(for: colorScheme).secondaryBG ) ) .onHover { hovering in withAnimation(.easeInOut(duration: 0.2)) { isHovered = hovering } } } .onAppear { // Lazy size calculation - only compute when row appears if cachedSize == nil { Task.detached(priority: .utility) { let url = URL(fileURLWithPath: plugin.path) let calculatedSize: Int64 if plugin.isDirectory { calculatedSize = totalSizeOnDisk(for: url) } else { calculatedSize = (try? url.resourceValues(forKeys: [.fileSizeKey]).fileSize.map { Int64($0) }) ?? 0 } await MainActor.run { onSizeCalculated(calculatedSize) } } } } .onChange(of: isExpanded) { expanded in if expanded { // Always search when expanding, clear previous results searchForRelatedFiles() } else { // Stop search if user collapses while searching searchEngine?.stop() } } } private func searchForRelatedFiles() { // Extract plugin name without extension let pluginName = (plugin.name as NSString).deletingPathExtension searchEngine = FileSearchEngine() isSearching = true relatedFiles = [] selectedRelatedFiles.removeAll() // Build search filters - search for plugin name let filters: [FilterType] = [.name(.contains, pluginName)] // Lazily extract bundle ID only when search is clicked (if not already loaded) var bundleIdToSearch: String? = plugin.bundleId if bundleIdToSearch == nil && plugin.isDirectory && plugin.path.hasSuffix(".driver") { let infoPlistPath = plugin.path + "/Contents/Info.plist" if let plistData = NSDictionary(contentsOfFile: infoPlistPath) { bundleIdToSearch = plistData["CFBundleIdentifier"] as? String } } // If bundle ID exists, also search for that if let bundleId = bundleIdToSearch, !bundleId.isEmpty { // Don't add bundle ID as a filter since we can only search for name // Instead, we'll do two searches - one for plugin name, one for bundle ID let bundleIdEngine = FileSearchEngine() // Search for bundle ID bundleIdEngine.search( rootPaths: locations.apps.paths, filters: [.name(.contains, bundleId)], includeSubfolders: false, includeHiddenFiles: false, caseSensitive: false, searchType: .filesAndFolders, excludeSystemFolders: true, onBatchFound: { results in DispatchQueue.main.async { // Add bundle ID results, avoiding duplicates let existingPaths = Set(self.relatedFiles.map { $0.url.path }) let newResults = results.filter { !existingPaths.contains($0.url.path) } self.relatedFiles.append(contentsOf: newResults) } }, completion: { } ) } // Search for plugin name across all app-related directories searchEngine?.search( rootPaths: locations.apps.paths, filters: filters, includeSubfolders: false, includeHiddenFiles: false, caseSensitive: false, searchType: .filesAndFolders, excludeSystemFolders: true, onBatchFound: { results in DispatchQueue.main.async { self.relatedFiles.append(contentsOf: results) } }, completion: { DispatchQueue.main.async { self.isSearching = false } } ) } private func toggleRelatedFileSelection(_ file: FileSearchResult) { if selectedRelatedFiles.contains(file.id) { selectedRelatedFiles.remove(file.id) } else { selectedRelatedFiles.insert(file.id) } } private func deletePluginAndRelatedFiles() { isPerformingAction = true Task { // Get selected related files let selectedFiles = relatedFiles.filter { selectedRelatedFiles.contains($0.id) } let relatedURLs = selectedFiles.map { $0.url } // If there are selected related files, delete them along with the plugin if !relatedURLs.isEmpty { let pluginURL = URL(fileURLWithPath: plugin.path) let allURLs = [pluginURL] + relatedURLs let bundleName = "\(plugin.category) Plugin - \(plugin.name) + \(relatedURLs.count) related file\(relatedURLs.count == 1 ? "" : "s")" let success = await withCheckedContinuation { continuation in DispatchQueue.global(qos: .userInitiated).async { let success = FileManagerUndo.shared.deleteFiles(at: allURLs, bundleName: bundleName) continuation.resume(returning: success) } } await MainActor.run { isPerformingAction = false if success { // Clear related files and selections relatedFiles.removeAll() selectedRelatedFiles.removeAll() onRefresh() } else { showCustomAlert( title: "Deletion Failed", message: "Failed to delete plugin and related files. They may require additional permissions or may not exist.", style: .critical ) } } } else { // No related files selected, just delete the plugin await MainActor.run { isPerformingAction = false onRemove() } } } } private func deleteRelatedFile(_ file: FileSearchResult) { Task { let success = await withCheckedContinuation { continuation in DispatchQueue.global(qos: .userInitiated).async { let success = FileManagerUndo.shared.deleteFiles( at: [file.url], bundleName: "Related File - \(file.name)" ) continuation.resume(returning: success) } } await MainActor.run { if success { // Remove from arrays relatedFiles.removeAll { $0.id == file.id } selectedRelatedFiles.remove(file.id) } else { showCustomAlert( title: "Deletion Failed", message: "Failed to delete '\(file.name)'. It may require additional permissions or may not exist.", style: .critical ) } } } } private var pluginColor: Color { return .purple } private var pluginIcon: String { return "waveform" } private func formatFileSize(_ size: Int64) -> String { let formatter = ByteCountFormatter() formatter.allowsNonnumericFormatting = false formatter.countStyle = .file return formatter.string(fromByteCount: size) } private func openInFinder(_ path: String) { let url = URL(fileURLWithPath: path) NSWorkspace.shared.activateFileViewerSelecting([url]) } } ================================================ FILE: Pearcleaner/Views/Settings/About.swift ================================================ // // About.swift // Pearcleaner // // Created by Alin Lupascu on 11/5/23. // import SwiftUI import AlinFoundation struct AboutSettingsTab: View { @EnvironmentObject var appState: AppState @Environment(\.colorScheme) var colorScheme @State private var disclose = false @State private var discloseCredits = false @State private var isResetting = false var body: some View { VStack(alignment: .center) { VStack(spacing: 10) { Image(nsImage: NSApp.applicationIconImage) Text(Bundle.main.name) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .font(.title) .bold() HStack { Text("Version \(Bundle.main.version)") Text("(Build \(Bundle.main.buildVersion))") .font(.footnote) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Text("Made with ❤️ by Alin Lupascu").foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText).font(.footnote) } .padding(.vertical, 50) VStack(spacing: 20) { // GitHub PearGroupBox(header: { Text("Support").foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText).font(.title) }, content: { HStack{ Image(systemName: "ant") .resizable() .scaledToFit() .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .frame(width: 20, height: 20) .padding(.trailing) VStack(alignment: .leading){ Text("Submit a bug or feature request") .font(.title3) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } Spacer() Button { NSWorkspace.shared.open(URL(string: "https://github.com/alienator88/Pearcleaner/issues/new/choose")!) } label: { Text("View") } .controlSize(.small) .buttonStyle(.plain) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) .padding(.vertical, 8) .padding(.horizontal, 14) .controlGroup(Capsule(style: .continuous), level: .primary) } }) // Translators PearGroupBox(header: { Text("Translation").foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText).font(.title) }, content: { HStack{ Image(systemName: "globe") .resizable() .scaledToFit() .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .frame(width: 20, height: 20) .padding(.trailing) VStack(alignment: .leading, spacing: 10){ Text("A **huge** thank you to everyone who has contributed so far!") .font(.title3) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Text(translators) .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } Spacer() Button { NSWorkspace.shared.open(URL(string: "https://github.com/alienator88/Pearcleaner/discussions/137")!) } label: { Text("View") } .controlSize(.small) .buttonStyle(.plain) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) .padding(.vertical, 8) .padding(.horizontal, 14) .controlGroup(Capsule(style: .continuous), level: .primary) } }) SettingsControlButtonGroup(isResetting: $isResetting, resetAction: { resetUserDefaults() }, exportAction: { exportUserDefaults() }, importAction: { importUserDefaults() }) } } } private func resetUserDefaults() { isResetting = true DispatchQueue.global(qos: .background).async { let keys = UserDefaults.standard.dictionaryRepresentation().keys .filter { $0.hasPrefix("settings.") } for key in keys { UserDefaults.standard.removeObject(forKey: key) } DispatchQueue.main.async { isResetting = false } } } private func exportUserDefaults() { let defaults = UserDefaults.standard.dictionaryRepresentation() let settingsOnly = defaults.filter { $0.key.hasPrefix("settings.") } guard let jsonData = try? JSONSerialization.data(withJSONObject: settingsOnly, options: [.prettyPrinted]) else { return } let savePanel = NSSavePanel() savePanel.directoryURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first savePanel.allowedContentTypes = [.json] savePanel.nameFieldStringValue = "PearcleanerSettings.json" savePanel.begin { response in guard response == .OK, let url = savePanel.url else { return } try? jsonData.write(to: url) } } private func importUserDefaults() { let openPanel = NSOpenPanel() openPanel.directoryURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first openPanel.allowedContentTypes = [.json] openPanel.begin { response in guard response == .OK, let url = openPanel.url, let data = try? Data(contentsOf: url), let dict = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else { return } for (key, value) in dict { UserDefaults.standard.setValue(value, forKey: key) } } } } let translators = "changanmoon, L1cardo, funsiyuan, megabitsenmzq, iFloneUEFN, matxpa, vogt65, AgiMaulana, kiwamizamurai, readingsnail, rokartur, MARCELOisME, exituser, Skro11-ru, Svec-Tomas, DrRoglaa, realkeremcam, Ihor-Khomenko, HungThinhIT" ================================================ FILE: Pearcleaner/Views/Settings/Folders.swift ================================================ // // Folders.swift // Pearcleaner // // Created by Alin Lupascu on 3/20/24. // import Foundation import SwiftUI import AppKit import AlinFoundation struct FolderSettingsTab: View { @EnvironmentObject var appState: AppState @EnvironmentObject var locations: Locations @EnvironmentObject var fsm: FolderSettingsManager @Environment(\.colorScheme) var colorScheme @State private var isHovered = false @State private var newKeyword: String = "" @AppStorage("settings.general.glass") private var glass: Bool = false @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true @AppStorage("settings.interface.scrollIndicators") private var scrollIndicators: Bool = false @AppStorage("settings.folders.defaultPathsLocked") private var defaultPathsLocked: Bool = true var body: some View { VStack(spacing: 20) { // === Application Folders============================================================================================ PearGroupBox(header: { HStack(alignment: .center, spacing: 0) { Text("Search these folders for applications").foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText).font(.title2) .padding(.leading, 5) Spacer() } }, content: { VStack { ScrollView { VStack(spacing: 5) { // Header row HStack(spacing: 8) { Text("Application Folder") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .padding(5) Spacer() } Divider().opacity(0.5) ForEach(fsm.folderPaths.indices.sorted(by: { fsm.folderPaths[$0] < fsm.folderPaths[$1] }), id: \.self) { index in HStack(spacing: 8) { Text(fsm.folderPaths[index]) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .font(.callout) .lineLimit(1) .truncationMode(.tail) .padding(5) Spacer() // Delete button Button(action: { if !(defaultPathsLocked && fsm.defaultPaths.contains(fsm.folderPaths[index])) { fsm.removePath(at: index) } }) { Image(systemName: "xmark.circle.fill") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .font(.system(size: 14)) } .buttonStyle(.plain) .help("Remove folder") .disabled(defaultPathsLocked && fsm.defaultPaths.contains(fsm.folderPaths[index])) .opacity(defaultPathsLocked && fsm.defaultPaths.contains(fsm.folderPaths[index]) ? 0.3 : 1) .frame(width: 24) } .background(Color.clear) if index != fsm.folderPaths.indices.last { Divider().opacity(0.5) } } } } .scrollIndicators(scrollIndicators ? .automatic : .never) .frame(height: 200) .clipShape(RoundedRectangle(cornerRadius: 10)) .onDrop(of: ["public.file-url"], isTargeted: nil) { providers -> Bool in providers.forEach { provider in provider.loadDataRepresentation(forTypeIdentifier: "public.file-url") { (data, error) in guard let data = data, error == nil, let url = URL(dataRepresentation: data, relativeTo: nil) else { printOS("FSM: Failed to load URL") return } var isDirectory: ObjCBool = false if FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory), isDirectory.boolValue { updateOnMain { fsm.addPath(url.path) } } else { printOS("FSM: The URL is not a directory: \(url.path)") } } } return true } HStack { Spacer() Text("Drop folders above or click to add").foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Button { selectFolder() } label: { EmptyView() } .buttonStyle(SimpleButtonStyle(icon: "plus.circle", help: String(localized: "Add folder"), color: ThemeColors.shared(for: colorScheme).secondaryText, size: 16, rotate: true)) // Button { // clipboardAdd() // } label: { EmptyView() } // .buttonStyle(SimpleButtonStyle(icon: "doc.on.clipboard", help: String(localized: "Add folder from clipboard"), color: ThemeColors.shared(for: colorScheme).secondaryText, size: 16, rotate: false)) Button { toggleDefaultPathsLock() } label: { EmptyView() } .buttonStyle(SimpleButtonStyle(icon: defaultPathsLocked ? "lock.fill" : "lock.open.fill", help: defaultPathsLocked ? "Unlock to remove default paths" : "Lock to restore default paths", color: ThemeColors.shared(for: colorScheme).secondaryText, size: 16, rotate: false)) Spacer() } } }) // === Orphaned Folders============================================================================================ PearGroupBox(header: { HStack(spacing: 0) { Text("Exclude these files and folders").foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText).font(.title2) .padding(.leading, 5) Spacer() } }, content: { VStack { ScrollView { VStack(spacing: 5) { // Compute combined list of all unique paths let allPaths = Array(Set(fsm.fileFolderPathsZ + fsm.fileFolderPathsApps)).sorted() if allPaths.isEmpty { HStack { Text("No files or folders added") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .font(.callout) .padding(5) Spacer() } .disabled(true) } else { // Header row with toggle labels HStack(spacing: 8) { Text("Path / Keyword") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .padding(5) Spacer() Text("Orphans") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .frame(width: 44, alignment: .center) Text("Apps") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .frame(width: 44, alignment: .center) // Spacing for delete button Color.clear.frame(width: 20) } Divider().opacity(0.5) } ForEach(allPaths, id: \.self) { path in HStack(spacing: 8) { Text(path) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .font(.callout) .lineLimit(1) .truncationMode(.tail) .padding(5) Spacer() // Orphans toggle Toggle("", isOn: Binding( get: { fsm.fileFolderPathsZ.contains(path) }, set: { enabled in if enabled { fsm.addPathZ(path) } else { fsm.removePathZ(path) } } )) .toggleStyle(.switch) .controlSize(.mini) .help("Exclude from orphaned file search") .frame(width: 44, alignment: .center) // Apps toggle Toggle("", isOn: Binding( get: { fsm.fileFolderPathsApps.contains(path) }, set: { enabled in if enabled { fsm.addPathApps(path) } else { fsm.removePathApps(path) } } )) .toggleStyle(.switch) .controlSize(.mini) .help("Exclude from app file search") .frame(width: 44, alignment: .center) // Delete button Button(action: { fsm.removePathZ(path) fsm.removePathApps(path) }) { Image(systemName: "xmark.circle.fill") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .font(.system(size: 14)) } .buttonStyle(.plain) .help("Remove from both lists") .frame(width: 24) } .background(Color.clear) if path != allPaths.last { Divider().opacity(0.5) } } } } .scrollIndicators(scrollIndicators ? .automatic : .never) .frame(height: 200) .clipShape(RoundedRectangle(cornerRadius: 10)) .onDrop(of: ["public.file-url"], isTargeted: nil) { providers -> Bool in providers.forEach { provider in provider.loadDataRepresentation(forTypeIdentifier: "public.file-url") { (data, error) in guard let data = data, error == nil, let url = URL(dataRepresentation: data, relativeTo: nil) else { printOS("FSM: Failed to load URL") return } updateOnMain { // Add to orphans by default fsm.addPathZ(url.path) } } } return true } TextField("Type a keyword to exclude, Enter ↵ to save", text: $newKeyword) .textFieldStyle(RoundedTextFieldStyle()) .padding(.horizontal, 20) .onSubmit { // Add to orphans by default fsm.addKeywordZ(newKeyword) newKeyword = "" } HStack { Spacer() Text("Drop files or folders above or click to add").foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Button { selectFilesFoldersZ() } label: { EmptyView() } .buttonStyle(SimpleButtonStyle(icon: "plus.circle", help: String(localized: "Add file/folder"), color: ThemeColors.shared(for: colorScheme).secondaryText, size: 16, rotate: true)) // Button { // clipboardAdd(zombie: true) // } label: { EmptyView() } // .buttonStyle(SimpleButtonStyle(icon: "doc.on.clipboard", help: String(localized: "Add file/folder from clipboard"), color: ThemeColors.shared(for: colorScheme).secondaryText, size: 16, rotate: false)) Button { fsm.removeAllPathsZ() fsm.removeAllPathsApps() } label: { EmptyView() } .buttonStyle(SimpleButtonStyle(icon: "trash", help: String(localized: "Remove all files/folders"), color: ThemeColors.shared(for: colorScheme).secondaryText, size: 16, rotate: false)) Spacer() } } }) } } private func selectFolder() { let dialog = NSOpenPanel() dialog.title = "Choose a folder" dialog.showsResizeIndicator = false dialog.showsHiddenFiles = false dialog.canChooseDirectories = true dialog.canCreateDirectories = true dialog.canChooseFiles = false if dialog.runModal() == NSApplication.ModalResponse.OK { if let result = dialog.url { fsm.addPath(result.path) } } else { return } } private func selectFilesFoldersZ() { let dialog = NSOpenPanel() dialog.title = "Choose files or folders" dialog.showsResizeIndicator = false dialog.showsHiddenFiles = true dialog.canChooseDirectories = true dialog.canCreateDirectories = false dialog.canChooseFiles = true if dialog.runModal() == NSApplication.ModalResponse.OK { if let result = dialog.url { fsm.addPathZ(result.path) } } else { return } } // private func clipboardAdd(zombie: Bool = false) { // let pasteboard = NSPasteboard.general // // // Check for file URL first // if let fileURL = pasteboard.propertyList(forType: .fileURL) as? String, // let folderURL = URL(string: fileURL) { // processClipboardPath(folderURL.path, zombie: zombie) // } // // Fallback to string-based path // else if let clipboardString = pasteboard.string(forType: .string) { // processClipboardPath(clipboardString, zombie: zombie) // } else { // printOS("FSM: Clipboard does not contain a valid path or file URL") // } // } // Helper function to process the extracted path // private func processClipboardPath(_ path: String, zombie: Bool) { // let fileManager = FileManager.default // var isDir: ObjCBool = false // // if fileManager.fileExists(atPath: path, isDirectory: &isDir) { // if zombie || isDir.boolValue { // zombie ? fsm.addPathZ(path) : fsm.addPath(path) // } else { // printOS("FSM: Clipboard content is not a directory and orphans mode is disabled") // } // } else { // printOS("FSM: Clipboard content is not a valid path") // } // } private func toggleDefaultPathsLock() { defaultPathsLocked.toggle() if defaultPathsLocked { // Re-locking: add back missing default paths for defaultPath in fsm.defaultPaths { if !fsm.folderPaths.contains(defaultPath) { fsm.addPath(defaultPath) } } } else { // Unlocking: show warning showCustomAlert( title: "Default Paths Unlocked", message: "You can now remove the default application paths.\n\nWarning: If all paths are removed, no applications will be found during scans.", style: .warning ) } } } class FolderSettingsManager: ObservableObject { static let shared = FolderSettingsManager() @Published var folderPaths: [String] = [] @Published var fileFolderPathsZ: [String] = [] @Published var fileFolderPathsApps: [String] = [] private let appsKey = "settings.folders.apps" private let zombieKey = "settings.folders.zombie" private let appsExclusionKey = "settings.folders.appsExclusion" let defaultPaths = ["/Applications", "\(NSHomeDirectory())/Applications"] init() { loadDefaultPathsIfNeeded() } // Application folders ////////////////////////////////////////////////////////////////////////////////// private func loadDefaultPathsIfNeeded() { var appsPaths = UserDefaults.standard.stringArray(forKey: appsKey) ?? defaultPaths let zombiePaths = UserDefaults.standard.stringArray(forKey: zombieKey) ?? [] let appsExclusionPaths = UserDefaults.standard.stringArray(forKey: appsExclusionKey) ?? [] if appsPaths.count < 2 { appsPaths = defaultPaths } UserDefaults.standard.set(appsPaths, forKey: appsKey) self.folderPaths = appsPaths self.fileFolderPathsZ = zombiePaths self.fileFolderPathsApps = appsExclusionPaths } func addPath(_ path: String) { if !self.folderPaths.contains(path) { self.folderPaths.append(path) UserDefaults.standard.set(self.folderPaths, forKey: appsKey) } } func removePath(at index: Int) { guard self.folderPaths.indices.contains(index) else { return } self.folderPaths.remove(at: index) // Update local state UserDefaults.standard.set(self.folderPaths, forKey: appsKey) } func removePath(_ path: String) { if let index = self.folderPaths.firstIndex(of: path) { self.folderPaths.remove(at: index) // Update local state UserDefaults.standard.set(self.folderPaths, forKey: appsKey) } } func refreshPaths() { self.folderPaths = UserDefaults.standard.stringArray(forKey: appsKey) ?? defaultPaths } func getPaths() -> [String] { return UserDefaults.standard.stringArray(forKey: appsKey) ?? defaultPaths } // Orphaned files ////////////////////////////////////////////////////////////////////////////////// func addPathZ(_ path: String) { let sanitizedPath = URL(fileURLWithPath: path).standardizedFileURL.path if !self.fileFolderPathsZ.contains(sanitizedPath) { self.fileFolderPathsZ.append(sanitizedPath) UserDefaults.standard.set(self.fileFolderPathsZ, forKey: zombieKey) } } func addKeywordZ(_ keyword: String) { if !self.fileFolderPathsZ.contains(keyword) { self.fileFolderPathsZ.append(keyword) UserDefaults.standard.set(self.fileFolderPathsZ, forKey: zombieKey) } } func removePathZ(at index: Int) { guard self.fileFolderPathsZ.indices.contains(index) else { return } self.fileFolderPathsZ.remove(at: index) // Update local state UserDefaults.standard.set(self.fileFolderPathsZ, forKey: zombieKey) } func removePathZ(_ path: String) { if let index = self.fileFolderPathsZ.firstIndex(of: path) { self.fileFolderPathsZ.remove(at: index) // Update local state UserDefaults.standard.set(self.fileFolderPathsZ, forKey: zombieKey) } } func removeAllPathsZ() { self.fileFolderPathsZ.removeAll() UserDefaults.standard.set([], forKey: zombieKey) } func refreshPathsZ() { self.fileFolderPathsZ = UserDefaults.standard.stringArray(forKey: zombieKey) ?? [] } func getPathsZ() -> [String] { return UserDefaults.standard.stringArray(forKey: zombieKey) ?? [] } // App file exclusions ////////////////////////////////////////////////////////////////////////////////// func addPathApps(_ path: String) { // Only standardize if it's an actual file path (starts with / or ~), not a keyword let sanitizedPath: String if path.hasPrefix("/") || path.hasPrefix("~") { sanitizedPath = URL(fileURLWithPath: path).standardizedFileURL.path } else { sanitizedPath = path } if !self.fileFolderPathsApps.contains(sanitizedPath) { self.fileFolderPathsApps.append(sanitizedPath) UserDefaults.standard.set(self.fileFolderPathsApps, forKey: appsExclusionKey) } } func addKeywordApps(_ keyword: String) { if !self.fileFolderPathsApps.contains(keyword) { self.fileFolderPathsApps.append(keyword) UserDefaults.standard.set(self.fileFolderPathsApps, forKey: appsExclusionKey) } } func removePathApps(at index: Int) { guard self.fileFolderPathsApps.indices.contains(index) else { return } self.fileFolderPathsApps.remove(at: index) UserDefaults.standard.set(self.fileFolderPathsApps, forKey: appsExclusionKey) } func removePathApps(_ path: String) { if let index = self.fileFolderPathsApps.firstIndex(of: path) { self.fileFolderPathsApps.remove(at: index) UserDefaults.standard.set(self.fileFolderPathsApps, forKey: appsExclusionKey) } } func removeAllPathsApps() { self.fileFolderPathsApps.removeAll() UserDefaults.standard.set([], forKey: appsExclusionKey) } func refreshPathsApps() { self.fileFolderPathsApps = UserDefaults.standard.stringArray(forKey: appsExclusionKey) ?? [] } func getPathsApps() -> [String] { return UserDefaults.standard.stringArray(forKey: appsExclusionKey) ?? [] } } ================================================ FILE: Pearcleaner/Views/Settings/General.swift ================================================ // // General.swift // Pearcleaner // // Created by Alin Lupascu on 11/5/23. // import Foundation import SwiftUI import FinderSync import AlinFoundation import UniformTypeIdentifiers struct GeneralSettingsTab: View { @EnvironmentObject var appState: AppState @EnvironmentObject var locations: Locations @Environment(\.colorScheme) var colorScheme @AppStorage("settings.sentinel.enable") private var sentinel: Bool = false @AppStorage("settings.general.brew") private var brew: Bool = false @AppStorage("settings.general.oneshot") private var oneShotMode: Bool = false @AppStorage("settings.general.confirmAlert") private var confirmAlert: Bool = false @AppStorage("settings.general.cli") private var isCLISymlinked = false @AppStorage("settings.general.namesearchstrict") private var nameSearchStrict = false @AppStorage("settings.general.spotlight") private var spotlight = false @AppStorage("settings.general.searchSensitivity") private var sensitivityLevel: SearchSensitivityLevel = .strict @AppStorage("settings.general.deepLevelAlertShown") private var deepLevelAlertShown: Bool = false @AppStorage("settings.general.searchTextContent") private var searchTextContent: Bool = false @AppStorage("settings.updater.loadOnStartup") private var loadUpdatesOnStartup: Bool = true @AppStorage("settings.general.sudoCacheTimeout") private var sudoCacheTimeoutData: Data = { let defaultTimeout = SudoCacheTimeout() return (try? JSONEncoder().encode(defaultTimeout)) ?? Data() }() @State private var showAppIconInMenu = UserDefaults.showAppIconInMenu @State private var showDeepAlert: Bool = false @State private var sudoCacheTimeout = SudoCacheTimeout() @FocusState private var isTimeoutFieldFocused: Bool var body: some View { VStack(spacing: 20) { // === Functionality ================================================================================================ PearGroupBox( header: { Text("Functionality").foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText).font(.title2) }, content: { VStack { HStack(spacing: 0) { Image(systemName: brew ? "mug.fill" : "mug") .resizable() .scaledToFit() .frame(width: 15, height: 15) .padding(.trailing) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText.opacity(1)) VStack(alignment: .leading, spacing: 0) { HStack(spacing: 0) { Text("Homebrew cleanup after uninstall") .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText.opacity(1)) InfoButton(text: String(localized: "When Homebrew cleanup is enabled, Pearcleaner will check if the app you are removing was installed via Homebrew and remove the cache to keep everything synced up.")) } } Spacer() Toggle(isOn: $brew, label: { }) .toggleStyle(SettingsToggle()) } .padding(5) HStack(spacing: 0) { Image(systemName: confirmAlert ? "exclamationmark.triangle.fill" : "exclamationmark.triangle") .resizable() .scaledToFit() .frame(width: 15, height: 15) .padding(.trailing) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) VStack(alignment: .leading, spacing: 5) { Text("Confirmation alerts") .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } InfoButton(text: String(localized: "When executing certain write actions, show an alert before proceeding.")) Spacer() Toggle(isOn: $confirmAlert, label: { }) .toggleStyle(SettingsToggle()) } .padding(5) HStack(spacing: 0) { Image(systemName: oneShotMode ? "scope" : "circlebadge") .resizable() .scaledToFit() .frame(width: 15, height: 15) .padding(.trailing) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) VStack(alignment: .leading, spacing: 5) { Text("Close after uninstall") .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } InfoButton(text: String(localized: "When this mode is enabled, clicking the Uninstall button to remove an app will also close Pearcleaner right after.\nThis only affects Pearcleaner when it is opened via external means, like Sentinel Trash Monitor, Finder extension or a Deep Link.\nThis allows for single use of the app for a quick uninstall. When Pearcleaner is opened normally, this setting is ignored and will work as usual.")) Spacer() Toggle(isOn: $oneShotMode, label: { }) .toggleStyle(SettingsToggle()) } .padding(5) HStack(spacing: 0) { Image(systemName: "arrow.down.circle") .resizable() .scaledToFit() .frame(width: 15, height: 15) .padding(.trailing) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) VStack(alignment: .leading, spacing: 5) { Text("Load app updates on startup") .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } InfoButton(text: String(localized: "When enabled, Pearcleaner will automatically scan for app updates in the background during launch. This way, updates are ready when you open the Updater view.")) Spacer() Toggle(isOn: $loadUpdatesOnStartup, label: { }) .toggleStyle(SettingsToggle()) } .padding(5) HStack(spacing: 0) { Image(systemName: "key.fill") .resizable() .scaledToFit() .frame(width: 15, height: 15) .padding(.trailing) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) VStack(alignment: .leading, spacing: 0) { HStack(spacing: 0) { Text("Password cache timeout") .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) InfoButton(text: String(localized: "When running privileged Homebrew operations, Pearcleaner caches your password in the macOS Keychain for this duration to avoid repeated password prompts. Homebrew commands cannot be executed with the privileged helper tool Pearcleaner offers.")) } } Spacer() HStack(spacing: 4) { TextField("", value: $sudoCacheTimeout.value, format: .number) .textFieldStyle(.roundedBorder) .frame(width: 50) .multilineTextAlignment(.center) .focusable(false) .focused($isTimeoutFieldFocused) Stepper { Text("") } onIncrement: { if sudoCacheTimeout.value < 999 { sudoCacheTimeout.value += 1 } isTimeoutFieldFocused = false } onDecrement: { if sudoCacheTimeout.value > 1 { sudoCacheTimeout.value -= 1 } isTimeoutFieldFocused = false } } Picker("", selection: $sudoCacheTimeout.unit) { ForEach(SudoCacheTimeout.TimeUnit.allCases, id: \.self) { unit in Text(unit.displayName(for: sudoCacheTimeout.value)).tag(unit) } } .pickerStyle(.menu) .frame(width: 100) Button { KeychainPasswordManager.shared.invalidateCache() } label: { Image(systemName: "xmark.circle") .font(.callout) } .buttonStyle(.plain) .padding(.leading, 5) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .help("Clear cached credentials immediately") } .padding(5) .onChange(of: sudoCacheTimeout) { newValue in sudoCacheTimeoutData = (try? JSONEncoder().encode(newValue)) ?? Data() } } } ) // === Search Sensitivity ===================================================================================================== PearGroupBox( header: { HStack(spacing: 0) { Text("Search Sensitivity").foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText).font(.title2) InfoButton(text: String(localized: """ The search sensitivity level controls how strict or lenient Pearcleaner is when finding related files for an app: • Strict – \(SearchSensitivityLevel.strict.description) • Enhanced – \(SearchSensitivityLevel.enhanced.description) • Deep – \(SearchSensitivityLevel.deep.description) At levels higher than Strict it is recommended to check found files manually. """)) Spacer() // Text content search toggle (Deep level only) if sensitivityLevel == .deep { HStack { Spacer() Button { searchTextContent.toggle() } label: { Text("Search file content") .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .buttonStyle(CircleCheckboxButtonStyle(isSelected: searchTextContent)) .padding(.horizontal, 5) } } Text(verbatim: "\(sensitivityLevel.title)") .font(.title3) .fontWeight(.semibold) .foregroundStyle(sensitivityLevel.color) .padding(4) .padding(.horizontal, 2) .background { RoundedRectangle(cornerRadius: 8) .fill(ThemeColors.shared(for: colorScheme).secondaryBG) } } }, content: { HStack { Text("Fewer files").textCase(.uppercase).font(.caption2).foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Slider(value: Binding( get: { Double(sensitivityLevel.rawValue) }, set: { sensitivityLevel = SearchSensitivityLevel(rawValue: Int($0)) ?? .strict } ), in: 0...Double(SearchSensitivityLevel.allCases.count - 1), step: 1) .tint(sensitivityLevel.color) .onChange(of: sensitivityLevel) { newLevel in if newLevel == .deep && !deepLevelAlertShown { showDeepAlert = true } } Text("Most files").textCase(.uppercase).font(.caption2).foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .padding(5) .alert("Deep Search Level", isPresented: $showDeepAlert) { Button("Okay") { deepLevelAlertShown = true } } message: { Text(SearchSensitivityLevel.deep.description) } }) // === Sentinel ===================================================================================================== PearGroupBox( header: { Text("Sentinel Monitor").foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText).font(.title2) }, content: { HStack(spacing: 0) { Image(systemName: sentinel ? "eye.circle" : "eye.slash.circle") .resizable() .scaledToFit() .frame(width: 15, height: 15) .padding(.trailing) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Text("Detect when apps are moved to Trash") .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) InfoButton(text: String(localized: "When applications are moved to Trash, Pearcleaner will launch and find related files and folders for deletion.")) Spacer() Toggle(isOn: $sentinel, label: { }) .toggleStyle(SettingsToggle()) .onChange(of: sentinel) { newValue in if newValue { launchctl(load: true) } else { launchctl(load: false) } } } .padding(5) }) // === Finder Extension ============================================================================================= PearGroupBox( header: { Text("Finder Extension").foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText).font(.title2) }, content: { VStack { HStack(spacing: 0) { Image(systemName: appState.finderExtensionEnabled ? "puzzlepiece.extension.fill" : "puzzlepiece.extension") .resizable() .scaledToFit() .frame(width: 15, height: 15) .padding(.trailing) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) VStack { HStack(spacing: 0) { Text("Enable context menu extension for Finder") .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) InfoButton(text: String(localized: "Enabling this extension will allow you to right click apps in Finder to quickly uninstall them with Pearcleaner")) Spacer() } HStack(spacing: 0) { Text("macOS only enables extensions if the main app is in Applications folder") .font(.footnote) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Button { FIFinderSyncController.showExtensionManagementInterface() } label: { Image(systemName: "gear") } .buttonStyle(.plain) .padding(.leading, 5) Spacer() } } Spacer() Toggle(isOn: $appState.finderExtensionEnabled, label: { }) .toggleStyle(SettingsToggle()) .onChange(of: appState.finderExtensionEnabled) { newValue in if newValue { manageFinderPlugin(install: true) } else { manageFinderPlugin(install: false) } } } if appState.finderExtensionEnabled { HStack(spacing: 0) { Image(systemName: "") .resizable() .scaledToFit() .frame(width: 15, height: 15) .padding(.trailing) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Text("Enable icon for Finder extension") .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Spacer() Toggle(isOn: $showAppIconInMenu, label: { }) .toggleStyle(SettingsToggle()) .onChange(of: showAppIconInMenu) { newValue in UserDefaults.showAppIconInMenu = newValue } } } } .padding(5) }) // === CLI ========================================================================================================== PearGroupBox( header: { Text("Command Line").foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText).font(.title2) }, content: { VStack { HStack(spacing: 0) { Image(systemName: "terminal") .resizable() .scaledToFit() .frame(width: 15, height: 15) .padding(.trailing) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) VStack { HStack { Text("Pearcleaner CLI support") .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) InfoButton(text: String(localized: "Enabling the CLI will allow you to execute Pearcleaner actions from the Terminal. This will add pearcleaner command into /usr/local/bin so it's available directly from your PATH environment variable. Try it after enabling:\n\n> pear --help")) Spacer() } if !HelperToolManager.shared.isHelperToolInstalled { HStack { Text("Helper tool needs to be enabled") .foregroundStyle(Color.red) .font(.footnote) Spacer() } } } Spacer() Toggle(isOn: $isCLISymlinked, label: { }) .toggleStyle(SettingsToggle()) .onChange(of: isCLISymlinked) { newValue in if newValue { manageSymlink(install: true) } else { manageSymlink(install: false) } } } } .padding(5) }) } .onAppear { Task { appState.updateExtensionStatus() fixLegacySymlink() isCLISymlinked = checkCLISymlink() } // Load sudo cache timeout from AppStorage if let decoded = try? JSONDecoder().decode(SudoCacheTimeout.self, from: sudoCacheTimeoutData) { sudoCacheTimeout = decoded } } } } // MARK: - SudoCacheTimeout struct SudoCacheTimeout: Codable, Equatable { var value: Int = 5 var unit: TimeUnit = .minutes enum TimeUnit: String, Codable, CaseIterable { case minutes = "Minutes" case hours = "Hours" case days = "Days" func displayName(for value: Int) -> String { switch self { case .minutes: return value == 1 ? "Minute" : "Minutes" case .hours: return value == 1 ? "Hour" : "Hours" case .days: return value == 1 ? "Day" : "Days" } } } var seconds: TimeInterval { switch unit { case .minutes: return TimeInterval(value * 60) case .hours: return TimeInterval(value * 3600) case .days: return TimeInterval(value * 86400) } } } enum SearchSensitivityLevel: Int, CaseIterable, Identifiable { case strict, enhanced, deep var id: Int { rawValue } var title: String { switch self { case .strict: return String(localized: "Strict") case .enhanced: return String(localized: "Enhanced") case .deep: return String(localized: "Deep") } } var color: Color { switch self { case .strict: return .orange case .enhanced: return .green case .deep: return .red } } var description: String { switch self { case .strict: return String(localized: "Exact string matches only for app name, bundle ID, and entitlements. Most conservative, recommended as default choice.") case .enhanced: return String(localized: "Everything in Strict plus partial string matches. May find a few unrelated files in some cases.") case .deep: return String(localized: "Everything in Enhanced plus adds company name and team identifier. Searches file contents, metadata, Finder comments, and files created by the app. Most comprehensive cleanup, finds all resources associated with the app and the developer, even other apps they create.") } } } ================================================ FILE: Pearcleaner/Views/Settings/Helper.swift ================================================ // // Helper.swift // Pearcleaner // // Created by Alin Lupascu on 3/14/25. // import SwiftUI import Foundation import AlinFoundation struct HelperSettingsTab: View { @Environment(\.colorScheme) var colorScheme @ObservedObject private var helperToolManager = HelperToolManager.shared @State private var commandOutput: String = "Command output will display here" @State private var commandToRun: String = "whoami" @State private var commandToRunManual: String = "" @State private var showTestingUI: Bool = false var body: some View { VStack(spacing: 20) { // === Frequency ============================================================================================ PearGroupBox( header: { HStack { Text("Management").foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText).font(.title2) Spacer() // Button(action: { } }, content: { VStack { HStack(spacing: 0) { Image(systemName: "key") .resizable() .scaledToFit() .frame(width: 20, height: 20) .padding(.trailing) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .onTapGesture { showTestingUI.toggle() } Text("Perform privileged operations seamlessly without password prompts") .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .frame(minWidth: 450, maxWidth: .infinity, alignment: .leading) // Spacer() Toggle(isOn: Binding( get: { helperToolManager.isHelperToolInstalled }, set: { newValue in Task { if newValue { await helperToolManager.manageHelperTool(action: .install) } else { await helperToolManager.manageHelperTool(action: .uninstall) } } } ), label: { }) .toggleStyle(SettingsToggle()) .frame(alignment: .trailing) } Divider() .padding(.vertical, 5) HStack { Text(helperToolManager.message) .font(.footnote) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() } } .padding(5) }) PearGroupBox(header: { Text("Information").foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText).font(.title2) }, content: { let message: LocalizedStringKey = """ Pearcleaner will ask you to enter your password once to enable the helper, then all subsequent privileged operations will run without any other prompts as long as the helper stays enabled in Settings > Login Items. This authorization is all managed by macOS via SMAppService. """ VStack(alignment: .leading, spacing: 20) { Text(message).foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText).font(.body).lineSpacing(5) Text("Since **AuthorizationExecuteWithPrivileges** has been deprecated by Apple as a less secure authentication method, it has been removed from Pearcleaner and the helper tool will be the only option going forward.").font(.footnote).foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } }) PearGroupBox(header: { Text("Helper Playground").foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText).font(.title2) }, content: { VStack { Picker("Example privileged commands", selection: $commandToRun) { Text(verbatim: "whoami").tag("whoami") Text(verbatim: "systemsetup -getsleep").tag("systemsetup -getsleep") Text(verbatim: "systemsetup -getcomputername").tag("systemsetup -getcomputername") } .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .pickerStyle(MenuPickerStyle()) .onChange(of: commandToRun) { newValue in if helperToolManager.isHelperToolInstalled { Task { let (success, output) = await helperToolManager.runCommand(commandToRun) if success { commandOutput = output } else { commandOutput = "Error: \(output)" } } } } .onAppear{ if helperToolManager.isHelperToolInstalled { Task { let (success, output) = await helperToolManager.runCommand(commandToRun) if success { commandOutput = output } else { commandOutput = "Error: \(output)" } } } } TextField("Enter manual command here, Enter to run", text: $commandToRunManual) .padding(8) .background(RoundedRectangle(cornerRadius: 8).strokeBorder(ThemeColors.shared(for: colorScheme).secondaryText.opacity(0.2), lineWidth: 1)) .textFieldStyle(.plain) .onSubmit { Task { let (success, output) = await helperToolManager.runCommand(commandToRunManual) if success { commandOutput = output } else { commandOutput = "Error: \(output)" } } } ScrollView { Text(commandOutput) .font(.system(.body, design: .monospaced)) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading) .padding() } .frame(height: 150) .frame(maxWidth: .infinity) .background(.tertiary.opacity(0.1)) .cornerRadius(8) Text("**whoami** command should return 'root' if helper is running correctly.").font(.footnote).foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .padding(.top, 5) } .padding(5) }) .disabled(!helperToolManager.isHelperToolInstalled) .opacity(helperToolManager.isHelperToolInstalled ? 1 : 0.5) } .onReceive(NotificationCenter.default.publisher(for: NSApplication.didBecomeActiveNotification)) { _ in Task { await helperToolManager.manageHelperTool() } if helperToolManager.isHelperToolInstalled && showTestingUI { Task { let (success, output) = await helperToolManager.runCommand(commandToRun) if success { commandOutput = output } else { printOS("Helper: \(output)") } } } } } } ================================================ FILE: Pearcleaner/Views/Settings/Interface.swift ================================================ // // Interface.swift // Pearcleaner // // Created by Alin Lupascu on 3/18/24. // import Foundation import SwiftUI import ServiceManagement import AlinFoundation struct InterfaceSettingsTab: View { @EnvironmentObject var appState: AppState @EnvironmentObject var locations: Locations @EnvironmentObject var updater: Updater @EnvironmentObject var permissionManager: PermissionManager @EnvironmentObject var fsm: FolderSettingsManager @Environment(\.colorScheme) var colorScheme @AppStorage("settings.general.glass") private var glass: Bool = false @AppStorage("settings.general.glassEffect") private var glassEffect: String = "Regular" @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true @AppStorage("settings.interface.minimalist") private var minimalEnabled: Bool = true @AppStorage("settings.interface.scrollIndicators") private var scrollIndicators: Bool = false @AppStorage("settings.interface.multiSelect") private var multiSelect: Bool = false @AppStorage("settings.interface.greetingEnabled") private var greetingEnabled: Bool = true @AppStorage("settings.interface.badgeOverlaysEnabled") private var badgeOverlaysEnabled: Bool = true @AppStorage("settings.interface.startupView") private var startupView: Int = CurrentPage.applications.rawValue @State private var showPagePopover: Bool = false @State private var hiddenPages: Set = AppState.loadHiddenPages() var body: some View { VStack(spacing: 20) { // === Appearance ================================================================================================= PearGroupBox(header: { Text("Appearance").foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText).font(.title2) }, content: { VStack { if #unavailable(macOS 26.0) { HStack(spacing: 0) { Image(systemName: glass ? "cube.transparent" : "cube.transparent.fill") .resizable() .scaledToFit() .frame(width: 15, height: 15) .padding(.trailing) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) VStack(alignment: .leading, spacing: 5) { Text("Transparent sidebar") .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } Spacer() Toggle(isOn: $glass, label: { }) .toggleStyle(SettingsToggle()) } .padding(5) } else { HStack(spacing: 0) { Image(systemName: "cube.transparent") .resizable() .scaledToFit() .frame(width: 15, height: 15) .padding(.trailing) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) VStack(alignment: .leading, spacing: 5) { Text("Glass Effect") .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } Spacer() Picker(selection: $glassEffect) { Text("Regular") .tag("Regular") Text("Clear") .tag("Clear") } label: { EmptyView() } .buttonStyle(.borderless) } .padding(5) } HStack(spacing: 0) { Image(systemName: animationEnabled ? "play" : "play.slash") .resizable() .scaledToFit() .frame(width: 15, height: 15) .padding(.trailing) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) VStack(alignment: .leading, spacing: 5) { Text(animationEnabled ? String(localized: "Transition animations enabled") : String(localized: "Transition animations disabled")) .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } Spacer() Toggle(isOn: $animationEnabled, label: { }) .toggleStyle(SettingsToggle()) } .padding(5) HStack(spacing: 0) { Image(systemName: scrollIndicators ? "computermouse.fill" : "computermouse") .resizable() .scaledToFit() .frame(width: 15, height: 15) .padding(.trailing) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) VStack(alignment: .leading, spacing: 5) { Text(scrollIndicators ? String(localized: "Scrollbar is set to OS preference in lists") : String(localized: "Scrollbar is hidden in lists")) .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } Spacer() Toggle(isOn: $scrollIndicators, label: { }) .toggleStyle(SettingsToggle()) } .padding(5) HStack(spacing: 0) { Image(systemName: minimalEnabled ? "list.dash.header.rectangle" : "list.bullet.rectangle") .resizable() .scaledToFit() .frame(width: 15, height: 15) .padding(.trailing) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) VStack(alignment: .leading, spacing: 5) { Text(minimalEnabled ? String(localized: "Simple app list enabled") : String(localized: "Simple app list disabled")) .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } Spacer() Toggle(isOn: $minimalEnabled, label: { }) .toggleStyle(SettingsToggle()) } .padding(5) HStack(spacing: 0) { Image(systemName: multiSelect ? "checkmark.square.fill" : "square") .resizable() .scaledToFit() .frame(width: 15, height: 15) .padding(.trailing) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) VStack(alignment: .leading, spacing: 5) { Text(multiSelect ? String(localized: "Multi-select enabled in sidebar app list") : String(localized: "Multi-select disabled in sidebar app list")) .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } Spacer() Toggle(isOn: $multiSelect, label: { }) .toggleStyle(SettingsToggle()) } .padding(5) HStack(spacing: 0) { Image(systemName: greetingEnabled ? "hand.raised.fill" : "hand.raised") .resizable() .scaledToFit() .frame(width: 15, height: 15) .padding(.trailing) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) VStack(alignment: .leading, spacing: 5) { Text(greetingEnabled ? String(localized: "Greeting enabled on main page") : String(localized: "Greeting disabled on main page")) .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } Spacer() Toggle(isOn: $greetingEnabled, label: { }) .toggleStyle(SettingsToggle()) } .padding(5) HStack(spacing: 0) { Image(systemName: badgeOverlaysEnabled ? "bell.badge.fill" : "bell.badge") .resizable() .scaledToFit() .frame(width: 15, height: 15) .padding(.trailing) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) VStack(alignment: .leading, spacing: 5) { Text(badgeOverlaysEnabled ? String(localized: "Badge notification overlays enabled") : String(localized: "Badge notification overlays disabled")) .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } Spacer() Toggle(isOn: $badgeOverlaysEnabled, label: { }) .toggleStyle(SettingsToggle()) } .padding(5) HStack(spacing: 0) { Image(systemName: "arrow.uturn.forward") .resizable() .scaledToFit() .frame(width: 15, height: 15) .padding(.trailing) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Text("Startup view & page visibility") .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Spacer() Button(action: { showPagePopover.toggle() }) { HStack(spacing: 6) { if let currentPage = CurrentPage(rawValue: startupView) { Image(systemName: currentPage.icon) Text(currentPage.title) } Image(systemName: "chevron.down") .font(.caption) } .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } .buttonStyle(.borderless) .popover(isPresented: $showPagePopover, arrowEdge: .trailing) { PageVisibilityPopover( startupView: $startupView, hiddenPages: $hiddenPages, colorScheme: colorScheme ) } } .padding(5) } }) PearGroupBox(header: { Text("Theme").foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText).font(.title2) }, content: { ThemeCustomizationView() }) } } } // MARK: - Page Visibility Popover struct PageVisibilityPopover: View { @Binding var startupView: Int @Binding var hiddenPages: Set let colorScheme: ColorScheme var body: some View { VStack(alignment: .leading, spacing: 8) { ForEach(CurrentPage.allCases, id: \.rawValue) { page in let isHidden = hiddenPages.contains(page.rawValue) let isStartupPage = startupView == page.rawValue let visiblePageCount = CurrentPage.allCases.count - hiddenPages.count HStack(spacing: 12) { // Radio button and label - clickable together Button(action: { // If trying to set a hidden page as startup, unhide it first if isHidden { hiddenPages.remove(page.rawValue) AppState.saveHiddenPages(hiddenPages) } startupView = page.rawValue }) { HStack(spacing: 8) { Image(systemName: isStartupPage ? "circle.fill" : "circle") .foregroundStyle(isStartupPage ? ThemeColors.shared(for: colorScheme).accent : ThemeColors.shared(for: colorScheme).secondaryText) Image(systemName: page.icon) .frame(width: 16) Text(page.title) .font(.body) } .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .contentShape(Rectangle()) } .buttonStyle(.plain) .help("Set as startup page") Spacer() // Eye toggle button Button(action: { if isHidden { hiddenPages.remove(page.rawValue) } else { hiddenPages.insert(page.rawValue) } AppState.saveHiddenPages(hiddenPages) }) { Image(systemName: isHidden ? "eye.slash.fill" : "eye.fill") .foregroundStyle(isHidden ? ThemeColors.shared(for: colorScheme).secondaryText.opacity(0.5) : ThemeColors.shared(for: colorScheme).accent) } .buttonStyle(.plain) .disabled(!isHidden && (visiblePageCount <= 1 || isStartupPage)) .help(isHidden ? "Show page" : (isStartupPage ? "Cannot hide active startup page" : "Hide page")) } .padding(.horizontal, 12) .padding(.vertical, 8) .opacity(isHidden ? 0.5 : 1.0) } } .frame(width: 200) .padding(.vertical, 8) } } ================================================ FILE: Pearcleaner/Views/Settings/SettingsWindow.swift ================================================ // // SettingsWindow.swift // Pearcleaner // // Created by Alin Lupascu on 10/31/23. // import SwiftUI import AlinFoundation struct SettingsView: View { @EnvironmentObject var appState: AppState @EnvironmentObject var fsm: FolderSettingsManager @Environment(\.colorScheme) var colorScheme @EnvironmentObject var updater: Updater @AppStorage("settings.general.glass") private var glass: Bool = false @AppStorage("settings.general.selectedTab") private var selectedTab: CurrentTabView = .general @AppStorage("settings.interface.scrollIndicators") private var scrollIndicators: Bool = false @State private var showPerms = false @State private var toolbarRefreshTrigger = false @ObservedObject private var helperToolManager = HelperToolManager.shared var body: some View { HStack(spacing: 0) { sidebarView .padding(8) detailView .padding(.top) } .ignoresSafeArea(edges: .top) .background(backgroundView(color: ThemeColors.shared(for: colorScheme).primaryBG)) .toolbarBackground(.hidden, for: .windowToolbar) .onAppear { // Force toolbar refresh by toggling state DispatchQueue.main.async { toolbarRefreshTrigger.toggle() } } .toolbar { ToolbarItem { Spacer() } // Conditional toolbar items based on selected tab ToolbarItemGroup { Group { switch selectedTab { case .helper: // Helper tab toolbar items Button { helperToolManager.openSMSettings() } label: { Label("Login Items", systemImage: "gear") .labelStyle(.iconOnly) .help("Login Items") } Button { Task { await helperToolManager.manageHelperTool(action: .uninstall) } } label: { Label("Unregister Service", systemImage: "trash") .labelStyle(.iconOnly) .help("Unregister Service") } Button { showCustomAlert(title: "Reset BTM", message: "This resets the whole Background Task Management database and will clear your 'Open at Login' and 'App Background Activity' list.", style: .warning, onOk: { Task { let _ = await helperToolManager.nuclearResetHelper() } }) } label: { Label("Nuclear Reset", systemImage: "exclamationmark.triangle") .labelStyle(.iconOnly) .help("Reset Background Task Management database") } // Button { // Task { // await helperToolManager.manageHelperTool(action: .reinstall) // } // } label: { // Label("Reinstall Service", systemImage: "arrow.clockwise") // .labelStyle(.iconOnly) // .help("Force Reinstall Service (fixes desync)") // } // #if DEBUG // Button { // Task { // let _ = await helperToolManager.nuclearResetHelper() // } // } label: { // Label("Nuclear Reset", systemImage: "exclamationmark.triangle") // .labelStyle(.iconOnly) // .help("Nuclear Reset (last resort - clears ALL helper instances)") // } // .foregroundStyle(.red) // #endif case .about: // About tab toolbar item Button(action: { NSWorkspace.shared.open(URL(string: "https://github.com/sponsors/alienator88")!) }, label: { Label { Text("Sponsor") .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .font(.body) .bold() } icon: { Image(systemName: "heart") .resizable() .scaledToFit() .frame(width: 16, height: 16) .foregroundStyle(.pink) } .labelStyle(.titleAndIcon) }) default: // No toolbar items for other tabs EmptyView() } } .id(toolbarRefreshTrigger) } } } /// Sidebar view for navigation items @ViewBuilder private var sidebarView: some View { VStack(alignment: .center, spacing: 4) { Spacer().frame(height: 35) VStack(alignment: .leading, spacing: 0) { SidebarItemView(title: CurrentTabView.general.title, systemImage: "gear", isSelected: selectedTab == .general) { selectedTab = .general } SidebarItemView(title: CurrentTabView.interface.title, systemImage: "macwindow", isSelected: selectedTab == .interface) { selectedTab = .interface } SidebarItemView(title: CurrentTabView.folders.title, systemImage: "folder", isSelected: selectedTab == .folders) { selectedTab = .folders } SidebarItemView(title: CurrentTabView.update.title, systemImage: "cloud", isSelected: selectedTab == .update) { selectedTab = .update } SidebarItemView(title: CurrentTabView.helper.title, systemImage: "key", isSelected: selectedTab == .helper) { selectedTab = .helper } SidebarItemView(title: CurrentTabView.about.title, systemImage: "info.circle", isSelected: selectedTab == .about) { selectedTab = .about } } Spacer() HStack { Button() { updateOnMain { selectedTab = .about } } label: { Text(verbatim: "v\(Bundle.main.version)".uppercased()) .font(.footnote) } Divider().frame(height: 10) Button() { showPerms.toggle() } label: { Text(String(localized: "Permissions").uppercased()) .font(.footnote) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .sheet(isPresented: $showPerms, content: { PermissionsSheetView() }) } .controlSize(.small) .buttonStyle(.plain) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .padding(.vertical, 8) .padding(.horizontal, 14) .controlGroup(Capsule(style: .continuous), level: .primary) } .padding(.bottom) .padding(.horizontal) .frame(width: 180) .ifGlassMain() } /// Detail view content based on the selected tab @ViewBuilder private var detailView: some View { ScrollView() { // The actual detail views wrapped inside the VStack switch selectedTab { case .general: GeneralSettingsTab() .environmentObject(appState) case .interface: InterfaceSettingsTab() case .folders: FolderSettingsTab() case .update: UpdateSettingsTab() .environmentObject(updater) case .helper: HelperSettingsTab() case .about: AboutSettingsTab() } } .scrollIndicators(scrollIndicators ? .automatic : .never) .frame(maxWidth: .infinity, maxHeight: .infinity) .padding() } } struct SidebarItemView: View { @Environment(\.colorScheme) var colorScheme var title: String var systemImage: String var isSelected: Bool var onTap: () -> Void var body: some View { HStack(spacing: 14) { Image(systemName: systemImage) .resizable() .aspectRatio(contentMode: .fit) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) .frame(width: 20, height: 20) Text(title) .font(.system(size: 14, weight: .regular)) .foregroundStyle(isSelected ? ThemeColors.shared(for: colorScheme).primaryText : ThemeColors.shared(for: colorScheme).secondaryText) if !HelperToolManager.shared.isHelperToolInstalled && title.lowercased().contains("helper") { Image(systemName: "exclamationmark.triangle") .resizable() .aspectRatio(contentMode: .fit) .foregroundStyle(.orange) .frame(width: 14, height: 14) .help("Please install the helper service") } Spacer() } .padding(.vertical, 8) .padding(.leading, 8) .frame(maxWidth: .infinity, alignment: .leading) .background(isSelected ? ThemeColors.shared(for: colorScheme).primaryText.opacity(0.2) : Color.clear) .cornerRadius(6) .contentShape(Rectangle()) .onTapGesture { onTap() } } } ================================================ FILE: Pearcleaner/Views/Settings/Update.swift ================================================ // // Update.swift // Pearcleaner // // Created by Alin Lupascu on 11/5/23. // import SwiftUI import Foundation import AlinFoundation struct UpdateSettingsTab: View { @EnvironmentObject var appState: AppState @EnvironmentObject var updater: Updater @Environment(\.colorScheme) var colorScheme var body: some View { VStack(spacing: 20) { // === Frequency ============================================================================================ PearGroupBox(header: { Text("Update Frequency").foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText).font(.title2) }, content: { FrequencyView(updater: updater) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) }) // === Release Notes ======================================================================================== PearGroupBox(header: { Text("Release Notes").foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText).font(.title2) }, content: { RecentReleasesView(updater: updater) .frame(height: 380) .frame(maxWidth: .infinity) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) }) // === Buttons ============================================================================================== HStack(alignment: .center, spacing: 20) { Button { updater.checkForUpdates(sheet: true, force: true) } label: { Label("Refresh", systemImage: "arrow.uturn.left.circle") } .buttonStyle(ControlGroupButtonStyle(foregroundColor: ThemeColors.shared(for: colorScheme).primaryText, shape: .capsule)) Button { updater.resetAnnouncementAlert() } label: { Label("Announcement", systemImage: "star") } .buttonStyle(ControlGroupButtonStyle(foregroundColor: ThemeColors.shared(for: colorScheme).primaryText, shape: .capsule)) .sheet(isPresented: $updater.showAnnouncementSheet, content: { updater.getAnnouncementView() }) Button { NSWorkspace.shared.open(URL(string: "https://github.com/alienator88/Pearcleaner/releases")!) } label: { Label("Releases", systemImage: "link") } .buttonStyle(ControlGroupButtonStyle(foregroundColor: ThemeColors.shared(for: colorScheme).primaryText, shape: .capsule)) } } } } ================================================ FILE: Pearcleaner/Views/ZombieView/ZombieSidebarView.swift ================================================ // // ZombieSidebarView.swift // Pearcleaner // // Created by Alin Lupascu on 8/9/25. // import Foundation import SwiftUI import AlinFoundation // Main zombie sidebar view struct ZombieSidebarView: View { @Binding var infoSidebar: Bool let displaySizeTotal: String let selectedCount: Int let totalCount: Int @ObservedObject var fsm: FolderSettingsManager @Binding var memoizedFiles: [URL] let onRestoreFile: (URL) -> Void @Environment(\.colorScheme) var colorScheme var body: some View { if infoSidebar { HStack { Spacer() VStack(alignment: .leading, spacing: 12) { ZombieSizeInfoSection(displaySizeTotal: displaySizeTotal, selectedCount: selectedCount, totalCount: totalCount) Divider() ZombieExcludedPathsSection(fsm: fsm, memoizedFiles: $memoizedFiles, onRestoreFile: onRestoreFile) Spacer() ZombieSidebarFooter() } .padding() .frame(width: 280) .ifGlassSidebar() .padding([.trailing, .bottom], 20) } .background(.black.opacity(0.00000000001)) .transition(.move(edge: .trailing)) .onTapGesture { infoSidebar = false } } } } // Size info component struct ZombieSizeInfoSection: View { let displaySizeTotal: String let selectedCount: Int let totalCount: Int @Environment(\.colorScheme) var colorScheme var body: some View { VStack(alignment: .leading, spacing: 8) { HStack(spacing: 0) { Text("Total Size:") .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Spacer() Text(displaySizeTotal) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } HStack(spacing: 0) { Text("Selected Items:") .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) Spacer() Text(verbatim: "\(selectedCount) / \(totalCount)") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } } } // Excluded paths section component struct ZombieExcludedPathsSection: View { @ObservedObject var fsm: FolderSettingsManager @Binding var memoizedFiles: [URL] let onRestoreFile: (URL) -> Void @Environment(\.colorScheme) var colorScheme private var sortedExcludedPaths: [String] { fsm.fileFolderPathsZ.sorted { $0.localizedCaseInsensitiveCompare($1) == .orderedAscending } } var body: some View { VStack(alignment: .leading, spacing: 8) { Text("Excluded Paths") .font(.subheadline) .fontWeight(.medium) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) if fsm.fileFolderPathsZ.isEmpty { Text("No paths excluded or linked to an app") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .italic() } else { ScrollView { LazyVStack(alignment: .leading, spacing: 4) { ForEach(sortedExcludedPaths, id: \.self) { path in ZombieExcludedPathRow(path: path, fsm: fsm, memoizedFiles: $memoizedFiles, onRestoreFile: onRestoreFile) } } } } } } } // Individual excluded path row component struct ZombieExcludedPathRow: View { @EnvironmentObject var appState: AppState @Environment(\.colorScheme) var colorScheme let path: String @ObservedObject var fsm: FolderSettingsManager @Binding var memoizedFiles: [URL] let onRestoreFile: (URL) -> Void @State private var isHovered = false private var associationInfo: (isAssociated: Bool, appName: String?) { let pathURL = URL(fileURLWithPath: path) for (appPath, associatedFiles) in ZombieFileStorage.shared.associatedFiles { if associatedFiles.contains(pathURL) { // File is associated, now get the app name let appName = appState.sortedApps.first(where: { $0.path == appPath })?.appName ?? appPath.lastPathComponent.replacingOccurrences(of: ".app", with: "") return (true, appName) } } return (false, nil) } var body: some View { HStack { HStack(spacing: 4) { VStack(alignment: .leading, spacing: 1) { HStack(spacing: 4) { Text(URL(fileURLWithPath: path).lastPathComponent) .font(.caption) .lineLimit(1) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) // Show link icon if this path is associated if associationInfo.isAssociated { Image(systemName: "link") .font(.caption2) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) } // Show associated app name in parentheses if let appName = associationInfo.appName { Text(verbatim: "(\(appName))") .font(.caption2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .lineLimit(1) } } .onHover { hovered in withAnimation(.easeInOut(duration: 0.2)) { isHovered = hovered } } if isHovered { Text(path) .font(.caption2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .lineLimit(nil) .transition(.opacity.combined(with: .move(edge: .top))) } } } Spacer() Button { let pathURL = URL(fileURLWithPath: path) // Remove from exclusion list fsm.removePathZ(path) // If this was an associated file, also remove the association if associationInfo.isAssociated { // Find which app this is associated with and remove the association for (appPath, associatedFiles) in ZombieFileStorage.shared.associatedFiles { if associatedFiles.contains(pathURL) { ZombieFileStorage.shared.removeAssociation(appPath: appPath, zombieFilePath: pathURL) break } } } // Restore file to memoized list onRestoreFile(pathURL) } label: { Image(systemName: "minus.circle") .foregroundStyle(.red) } .buttonStyle(.borderless) .help(associationInfo.isAssociated ? "Remove association and exclusion" : "Remove from exclusion list") } .padding(8) .background(ThemeColors.shared(for: colorScheme).secondaryText.opacity(0.1)) .cornerRadius(6) } } // Footer component struct ZombieSidebarFooter: View { var body: some View { @Environment(\.colorScheme) var colorScheme HStack { Text("Click to dismiss") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() } } } ================================================ FILE: Pearcleaner/Views/ZombieView/ZombieView.swift ================================================ // // ZombieView.swift // Pearcleaner // // Created by Alin Lupascu on 2/26/24. // import Foundation import SwiftUI import AlinFoundation struct ZombieView: View { @EnvironmentObject var appState: AppState @EnvironmentObject var locations: Locations @EnvironmentObject var fsm: FolderSettingsManager @ObservedObject private var consoleManager = GlobalConsoleManager.shared @State private var showPop: Bool = false @State private var windowController = WindowManager() @AppStorage("settings.general.leftoverWarning") private var warning: Bool = false @State private var showAlert = false @AppStorage("settings.general.glass") private var glass: Bool = false @AppStorage("settings.sentinel.enable") private var sentinel: Bool = false @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true @AppStorage("settings.general.selectedSort") var selectedSort: SortOptionList = .name @AppStorage("settings.interface.zombieListViewMode") private var viewMode: FileListViewMode = .simple @AppStorage("settings.general.confirmAlert") private var confirmAlert: Bool = false @AppStorage("settings.interface.scrollIndicators") private var scrollIndicators: Bool = false @Environment(\.colorScheme) var colorScheme @State private var searchZ: String = "" @State private var selectedZombieItemsLocal: Set = [] @State private var memoizedFiles: [URL] = [] @State private var collapsedCategories: Set = [] @State private var lastSearchTermUsed: String? = nil @State private var totalSize: Int64 = 0 @State private var totalSizeUninstallBtn: String = "" @State private var infoSidebar: Bool = false @State private var lastRefreshDate: Date? @State private var isRefreshing: Bool = false @State private var currentSearcher: ReversePathsSearcher? // Categorized files grouped by category var categorizedZombieFiles: [GroupedFiles] { // Group files by category var grouped: [FileCategory: [URL]] = [:] for file in memoizedFiles { let category = categorizeFile(file) grouped[category, default: []].append(file) } // Convert to GroupedFiles array return grouped.map { category, files in let totalSize = files.reduce(0) { sum, url in sum + (appState.zombieFile.fileSize[url] ?? 0) } let selectedCount = files.filter { selectedZombieItemsLocal.contains($0) }.count return GroupedFiles( category: category, files: files, totalSize: totalSize, isExpanded: !collapsedCategories.contains(category), allSelected: selectedCount == files.count && files.count > 0, someSelected: selectedCount > 0 && selectedCount < files.count ) } .sorted { $0.category.sortOrder < $1.category.sortOrder } } var body: some View { VStack(alignment: .center) { ZStack { VStack(spacing: 0) { // Search bar HStack { Image(systemName: "magnifyingglass") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) TextField("Search...", text: $searchZ) .onChange(of: searchZ) { newValue in updateMemoizedFiles(for: newValue, selectedSort: selectedSort) } .textFieldStyle(.plain) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) if !searchZ.isEmpty { Button { searchZ = "" } label: { Image(systemName: "xmark.circle.fill") .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } .buttonStyle(.plain) } } .padding(.horizontal, 12) .padding(.vertical, 8) .foregroundStyle(ThemeColors.shared(for: colorScheme).accent) .controlGroup(Capsule(style: .continuous), level: .primary) .padding(.top, 5) // Stats header HStack { Text("\(memoizedFiles.count) file\(memoizedFiles.count == 1 ? "" : "s")") .font(.caption) .monospacedDigit() .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) if appState.showProgress { Text("Scanning...") .font(.caption) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) ProgressView().controlSize(.mini) } Spacer() if let lastRefresh = lastRefreshDate { TimelineView(.periodic(from: lastRefresh, by: 1.0)) { _ in Text("Updated \(formatRelativeTime(lastRefresh))") .font(.caption) .monospacedDigit() .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } } .padding(.vertical) if memoizedFiles.isEmpty && !appState.showProgress { VStack { Spacer() Text("No orphaned files found") .font(.title2) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Text("Sentinel Monitor found no orphaned files") .font(.callout) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) } else { ScrollView() { if viewMode == .simple { // Simple flat list view LazyVStack(spacing: 0) { ForEach(Array(memoizedFiles.enumerated()), id: \.element) { index, file in if let fileSize = appState.zombieFile.fileSize[file], let fileIcon = appState.zombieFile.fileIcon[file], let iconImage = fileIcon.map(Image.init(nsImage:)) { VStack(spacing: 0) { ZombieFileDetailsItem(size: fileSize, icon: iconImage, path: file, memoizedFiles: $memoizedFiles, isSelected: self.binding(for: file)) if index < memoizedFiles.count - 1 { Divider() } } } } } } else { // Categorized view LazyVStack(spacing: 8) { ForEach(categorizedZombieFiles, id: \.category) { group in ZombieFileCategoryView( group: group, onToggleExpand: { withAnimation(.easeInOut(duration: animationEnabled ? 0.2 : 0)) { toggleZombieCategory(group.category) } }, onToggleSelection: { toggleZombieCategorySelection(group) }, fileItemBinding: binding(for:), memoizedFiles: $memoizedFiles ) if group.category != categorizedZombieFiles.last?.category { Divider() .padding(.vertical, 4) } } } } } .scrollIndicators(scrollIndicators ? .automatic : .never) } if appState.trashError { InfoButton(text: "A trash error has occurred, please open the debug window(⌘+D) to see what went wrong or try again", color: .orange, label: "View Error", warning: true, extraView: { Button("View Debug Window") { windowController.open(with: ConsoleView(), width: 600, height: 400) } }) .onDisappear { appState.trashError = false } .padding(.bottom) } } .opacity(infoSidebar ? 0.5 : 1) .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(.horizontal, 20) .safeAreaInset(edge: .bottom) { if !selectedZombieItemsLocal.isEmpty { HStack { Spacer() HStack(spacing: 10) { Button(selectedZombieItemsLocal.count == memoizedFiles.count ? "Deselect All" : "Select All") { if selectedZombieItemsLocal.count == memoizedFiles.count { selectedZombieItemsLocal.removeAll() } else { selectedZombieItemsLocal = Set(memoizedFiles) } updateTotalSizes() } .buttonStyle(ControlGroupButtonStyle( foregroundColor: ThemeColors.shared(for: colorScheme).accent, shape: Capsule(style: .continuous), level: .primary, skipControlGroup: true )) Divider().frame(height: 10) Menu { Button("Exclude \(selectedZombieItemsLocal.count) Selected") { excludeAllSelectedItems() } .help("This will exclude selected items from future scans. Exclusion list can be edited from Settings > Folders tab or the sidebar in this view.") Menu("Link Selected to App") { ForEach(appState.sortedApps, id: \.id) { app in Button(app.appName) { linkSelectedItemsToApp(app.path) } } } .help("Link all selected items to the chosen app and remove from orphan scans.") } label: { Text("Actions") } .buttonStyle(ControlGroupButtonStyle( foregroundColor: ThemeColors.shared(for: colorScheme).accent, shape: Capsule(style: .continuous), level: .primary, skipControlGroup: true )) Divider().frame(height: 10) Button { handleUninstallAction() } label: { Label { Text("Delete \(selectedZombieItemsLocal.count) Selected") } icon: { Image(systemName: "trash") } } .buttonStyle(ControlGroupButtonStyle( foregroundColor: ThemeColors.shared(for: colorScheme).accent, shape: Capsule(style: .continuous), level: .primary, skipControlGroup: true )) } .controlGroup(Capsule(style: .continuous), level: .primary) Spacer() } .padding([.horizontal, .bottom]) } } ZombieSidebarView( infoSidebar: $infoSidebar, displaySizeTotal: displaySizeTotal, selectedCount: selectedZombieItemsLocal.count, totalCount: memoizedFiles.count, fsm: fsm, memoizedFiles: $memoizedFiles, onRestoreFile: restoreFileToZombieList ) } .animation(animationEnabled ? .spring(response: 0.35, dampingFraction: 0.8) : .none, value: infoSidebar) .transition(.opacity) .onAppear { if lastRefreshDate == nil { lastRefreshDate = Date() } } .onChange(of: appState.zombieFile.fileSize) { _ in // Update memoized files whenever new files are added updateMemoizedFiles(for: searchZ, selectedSort: selectedSort, force: true) } .sheet(isPresented: $showAlert, content: { VStack(spacing: 10) { Text("Important") .font(.headline) Divider() Spacer() Text("Orphaned file search is not 100% accurate as it doesn't have any uninstalled app bundles to check against for file exclusion. This does a best guess search for files/folders and excludes the ones that have overlap with your currently installed applications. Please confirm files marked for deletion really do belong to uninstalled applications.") .font(.subheadline) Spacer() Button("Close") { warning = true showAlert = false // Start the file scan after user acknowledges the warning startFileScan() } .buttonStyle(SimpleButtonStyle(icon: "x.circle.fill", label: String(localized: "Close"), help: String(localized: "Dismiss"))) Spacer() } .padding(15) .frame(width: 400, height: 250) .background(GlassEffect(material: .hudWindow, blendingMode: .behindWindow)) }) } .onAppear { if !warning { showAlert = true } else { // Only trigger scan if warning has been acknowledged startFileScan() } } .onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("ZombieViewShouldRefresh"))) { _ in // Refresh orphaned files search startSearch() } .toolbarBackground(.hidden, for: .windowToolbar) .toolbar { TahoeToolbarItem(placement: .navigation) { VStack(alignment: .leading){ Text("Orphaned Files").foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText).font(.title2).fontWeight(.bold) Text("Remaining files and folders from previous applications") .font(.callout).foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) } } ToolbarItem { Spacer() } TahoeToolbarItem(isGroup: true) { Button { withAnimation(.spring(response: 0.4, dampingFraction: 0.8)) { consoleManager.showConsole.toggle() } } label: { Label("Console", systemImage: consoleManager.showConsole ? "terminal.fill" : "terminal") } .help("Toggle console output") Button { viewMode = viewMode == .simple ? .categorized : .simple } label: { Label("View", systemImage: viewMode == .simple ? "list.bullet" : "checklist") } .help(viewMode == .simple ? "Switch to categorized view" : "Switch to simple view") Button { // Cycle through sort options let allOptions = SortOptionList.allCases if let currentIndex = allOptions.firstIndex(of: selectedSort) { let nextIndex = (currentIndex + 1) % allOptions.count selectedSort = allOptions[nextIndex] updateMemoizedFiles(for: searchZ, selectedSort: selectedSort, force: true) } } label: { Label(selectedSort.title, systemImage: selectedSort.systemImage) } .help("Sort by \(selectedSort.title). Click to cycle through options") if appState.showProgress { Button { stopSearch() } label: { Label("Stop", systemImage: "stop.circle") } } else { Button { startSearch() } label: { Label("Refresh", systemImage: "arrow.counterclockwise") } } Button { if GlobalConsoleManager.shared.showConsole { GlobalConsoleManager.shared.showConsole.toggle() } infoSidebar.toggle() } label: { Label("Info", systemImage: "sidebar.trailing") } .help("See details") } } } private func startSearch() { GlobalConsoleManager.shared.appendOutput("Starting orphan file search...\n", source: CurrentPage.orphans.title) updateOnMain { appState.zombieFile = .empty appState.showProgress = true selectedZombieItemsLocal.removeAll() let searcher = ReversePathsSearcher(appState: appState, locations: locations, fsm: fsm, sortedApps: appState.sortedApps, streamingMode: true) currentSearcher = searcher searcher.reversePathsSearch { updateOnMain { self.lastRefreshDate = Date() self.currentSearcher = nil GlobalConsoleManager.shared.appendOutput("✓ Completed orphan file search\n", source: CurrentPage.orphans.title) } } } } private func stopSearch() { GlobalConsoleManager.shared.appendOutput("Stopping orphan search...\n", source: CurrentPage.orphans.title) currentSearcher?.stop() updateOnMain { appState.showProgress = false currentSearcher = nil GlobalConsoleManager.shared.appendOutput("✓ Orphan search stopped\n", source: CurrentPage.orphans.title) } } private func startFileScan() { startSearch() } private func handleUninstallAction() { showCustomAlert(enabled: confirmAlert, title: String(localized: "Warning"), message: String(localized: "Are you sure you want to remove these files?"), style: .warning, onOk: { Task { GlobalConsoleManager.shared.appendOutput("Starting deletion of \(selectedZombieItemsLocal.count) orphaned file(s)...\n", source: CurrentPage.orphans.title) let selectedItemsArray = Array(selectedZombieItemsLocal) let bundleName = "Orphaned Files (\(selectedItemsArray.count))" let result = FileManagerUndo.shared.deleteFiles(at: selectedItemsArray, bundleName: bundleName) if result { GlobalConsoleManager.shared.appendOutput("✓ Completed deletion of \(selectedItemsArray.count) orphaned file(s)\n", source: CurrentPage.orphans.title) if selectedZombieItemsLocal.count == appState.zombieFile.fileSize.keys.count { updateOnMain { appState.zombieFile = .empty searchZ = "" withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { appState.currentView = .empty } } } else { // Remove items from the list appState.zombieFile.fileSize = appState.zombieFile.fileSize.filter { !selectedZombieItemsLocal.contains($0.key) } appState.zombieFile.fileIcon = appState.zombieFile.fileIcon.filter { !selectedZombieItemsLocal.contains($0.key) } // Clear the selection selectedZombieItemsLocal.removeAll() // Update memoized files and total sizes updateMemoizedFiles(for: searchZ, selectedSort: selectedSort, force: true) } } } }) } private func binding(for file: URL) -> Binding { Binding( get: { // Only return true if the file is both selected AND still in memoizedFiles self.selectedZombieItemsLocal.contains(file) && memoizedFiles.contains(file) }, set: { isSelected in if isSelected { // Only allow selection if the file is in memoizedFiles if memoizedFiles.contains(file) { self.selectedZombieItemsLocal.insert(file) } } else { self.selectedZombieItemsLocal.remove(file) } updateTotalSizes() } ) } // The "Select All" toggle binding private var selectAllBinding: Binding { Binding( get: { if searchZ.isEmpty { return selectedZombieItemsLocal.count == appState.zombieFile.fileSize.count } else { // Only consider files that are currently in memoizedFiles let currentlyVisibleFiles = Set(memoizedFiles) let selectedVisibleFiles = selectedZombieItemsLocal.intersection(currentlyVisibleFiles) return !currentlyVisibleFiles.isEmpty && selectedVisibleFiles.count == currentlyVisibleFiles.count } }, set: { newValue in if newValue { // Only select files that are currently in memoizedFiles selectedZombieItemsLocal = Set(memoizedFiles) } else { // Only deselect files that are currently in memoizedFiles let filesToDeselect = Set(memoizedFiles) selectedZombieItemsLocal.subtract(filesToDeselect) } updateTotalSizes() } ) } private func updateMemoizedFiles(for searchTerm: String, selectedSort: SortOptionList, force: Bool = false) { if !force && searchTerm == lastSearchTermUsed && self.selectedSort == selectedSort { return } let results = filterAndSortFiles(for: searchTerm, selectedSort: selectedSort) memoizedFiles = results.files totalSize = results.totalSize lastSearchTermUsed = searchTerm self.selectedSort = selectedSort updateTotalSizes() } private func filterAndSortFiles(for searchTerm: String, selectedSort: SortOptionList) -> (files: [URL], totalSize: Int64) { let fileSize = appState.zombieFile.fileSize let filteredFiles = fileSize.filter { url, _ in searchTerm.isEmpty || url.lastPathComponent.localizedCaseInsensitiveContains(searchTerm) } let sortedFilteredFiles = filteredFiles.sorted { (left, right) -> Bool in switch selectedSort { case .name: return left.key.lastPathComponent.pearFormat() < right.key.lastPathComponent.pearFormat() case .size: return left.value > right.value case .path: return left.key.path.localizedCaseInsensitiveCompare(right.key.path) == .orderedAscending } }.map(\.key) let totalSize = filteredFiles.values.reduce(0, +) return (sortedFilteredFiles, totalSize) } func calculateTotalSelectedZombieSize() -> String { var total: Int64 = 0 for url in selectedZombieItemsLocal { let size = appState.zombieFile.fileSize[url] ?? 0 total += size } return formatByte(size: total).human } private func updateTotalSizes() { totalSizeUninstallBtn = calculateTotalSelectedZombieSize() } private var displaySizeText: String { return totalSizeUninstallBtn } private var displaySizeTotal: String { return formatByte(size: totalSize).human } private func excludeAllSelectedItems() { for path in selectedZombieItemsLocal { // Add to fsm path fsm.addPathZ(path.path) // Remove from memoizedFiles memoizedFiles.removeAll { $0 == path } // Remove from appState zombie file details appState.zombieFile.fileSize.removeValue(forKey: path) appState.zombieFile.fileIcon.removeValue(forKey: path) } // Clear all selected items selectedZombieItemsLocal.removeAll() } private func linkSelectedItemsToApp(_ appPath: URL) { for path in selectedZombieItemsLocal { // Add association (same logic as individual file linking) ZombieFileStorage.shared.addAssociation(appPath: appPath, zombieFilePath: path) // Exclude from future scans (same as exclude button) fsm.addPathZ(path.path) // Remove from memoizedFiles memoizedFiles.removeAll { $0 == path } // Remove from appState zombie file details appState.zombieFile.fileSize.removeValue(forKey: path) appState.zombieFile.fileIcon.removeValue(forKey: path) } // Clear all selected items selectedZombieItemsLocal.removeAll() } private func restoreFileToZombieList(_ fileURL: URL) { // Add back to zombie file data if it exists if FileManager.default.fileExists(atPath: fileURL.path) { if let fileSize = getFileSize(path: fileURL) { appState.zombieFile.fileSize[fileURL] = fileSize appState.zombieFile.fileIcon[fileURL] = getFileIcon(for: fileURL) } // Add back to memoized files if it matches current search if (searchZ.isEmpty || fileURL.lastPathComponent.localizedCaseInsensitiveContains(searchZ)) && !memoizedFiles.contains(fileURL) { memoizedFiles.append(fileURL) // Re-sort to maintain proper order updateMemoizedFiles(for: searchZ, selectedSort: selectedSort, force: true) } } } private func getFileSize(path: URL) -> Int64? { do { let resourceValues = try path.resourceValues(forKeys: [.fileSizeKey]) return Int64(resourceValues.fileSize ?? 0) } catch { return nil } } private func getFileIcon(for url: URL) -> NSImage? { return NSWorkspace.shared.icon(forFile: url.path) } // Helper function to toggle category expansion private func toggleZombieCategory(_ category: FileCategory) { if collapsedCategories.contains(category) { collapsedCategories.remove(category) } else { collapsedCategories.insert(category) } } // Helper function to toggle category selection private func toggleZombieCategorySelection(_ group: GroupedFiles) { if group.allSelected { // Deselect all files in this category for file in group.files { selectedZombieItemsLocal.remove(file) } } else { // Select all files in this category (that are in memoizedFiles) let filesToSelect = group.files.filter { memoizedFiles.contains($0) } for file in filesToSelect { selectedZombieItemsLocal.insert(file) } } updateTotalSizes() } } struct ZombieFileDetailsItem: View { @EnvironmentObject var appState: AppState @EnvironmentObject var fsm: FolderSettingsManager @Environment(\.colorScheme) var colorScheme @State private var isHovered = false @AppStorage("settings.interface.animationEnabled") private var animationEnabled: Bool = true let size: Int64? let icon: Image? let path: URL @Binding var memoizedFiles: [URL] @Binding var isSelected: Bool var body: some View { HStack(alignment: .center, spacing: 15) { // Selection checkbox Button(action: { isSelected.toggle() }) { EmptyView() } .buttonStyle(CircleCheckboxButtonStyle(isSelected: isSelected)) if let appIcon = icon { appIcon .resizable() .aspectRatio(contentMode: .fit) .frame(width: 30, height: 30) .clipShape(RoundedRectangle(cornerRadius: 8)) } VStack(alignment: .leading, spacing: 5) { HStack(alignment: .center) { Text(showLocalized(url: path)) .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) .font(.title3) .lineLimit(1) .truncationMode(.tail) .help(path.lastPathComponent) .overlay { if isHovered { VStack { Spacer() RoundedRectangle(cornerRadius: 10) .fill(ThemeColors.shared(for: colorScheme).primaryText.opacity(0.5)) .frame(height: 1.5) .offset(y: 3) } } } if let imageView = folderImages(for: path.path) { imageView } } path.path.pathWithArrows(separatorColor: ThemeColors.shared(for: colorScheme).primaryText) .font(.footnote) .foregroundStyle(ThemeColors.shared(for: colorScheme).secondaryText) .help(path.path) } .onTapGesture { NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path) } .onHover { hovering in withAnimation(Animation.easeInOut(duration: animationEnabled ? 0.35 : 0)) { self.isHovered = hovering } } Spacer() Text(verbatim: "\(formatByte(size: size!).human)") .foregroundStyle(ThemeColors.shared(for: colorScheme).primaryText) } .padding(.vertical, 8) .contextMenu { if path.pathExtension == "app" { Button("Open \(path.deletingPathExtension().lastPathComponent)") { NSWorkspace.shared.open(path) } Divider() } Button("Copy Path") { copyToClipboard(text: path.path) } Button("View in Finder") { NSWorkspace.shared.selectFile(path.path, inFileViewerRootedAtPath: path.deletingLastPathComponent().path) } Divider() Menu("Link To") { ForEach(appState.sortedApps, id: \.id) { app in let isAssociated = ZombieFileStorage.shared.getAssociatedFiles(for: app.path).contains(path) Button { if isAssociated { // Unlinking - remove association and unexclude ZombieFileStorage.shared.removeAssociation(appPath: app.path, zombieFilePath: path) fsm.removePathZ(path.path) } else { // Linking - add association and exclude (same as exclude button) ZombieFileStorage.shared.addAssociation(appPath: app.path, zombieFilePath: path) // Run the same exclude code fsm.addPathZ(path.path) memoizedFiles.removeAll { $0 == path } appState.zombieFile.fileSize.removeValue(forKey: path) appState.zombieFile.fileIcon.removeValue(forKey: path) if isSelected { isSelected = false } } } label: { HStack { Text(app.appName) if isAssociated { Image(systemName: "checkmark") } } } } } Button("Exclude") { fsm.addPathZ(path.path) // Remove from memoizedFiles memoizedFiles.removeAll { $0 == path } // Also remove from selectedZombieItemsLocal if it exists appState.zombieFile.fileSize.removeValue(forKey: path) appState.zombieFile.fileIcon.removeValue(forKey: path) // Use @EnvironmentObject to access the parent's selectedZombieItemsLocal if isSelected { isSelected = false } } .help("This adds the file/folder to the Exclusions list. Edit the exclusions list from Settings > Folders tab") } } } ================================================ FILE: Pearcleaner/announcements.json ================================================ { "4.4.0": "- Privileged Helper Service: Pearcleaner now supports using a privileged helper service to perform operations that require administrative permissions. This will eventually replace the current mechanism which uses Authorization Services as it's deprecated by Apple. More information in Settings > Helper tab.|- App Lipo: There's a new Lipo(beta) view available in the menu which will show all universal apps and strip the app binary of the unused architectures to save some space. Universal apps will also show a Lipo button on the App Details view next to the Uninstall button to perform the same procedure on each app individually.", "3.9.2": "- Multilingual Support: If you would like to help translate Pearcleaner to your language, please see GitHub issue #83", "3.7.0": "- App List Size Sorting: You can now sort the sidebar app list alphabetically or by descending size. Click the User/System header to toggle or from the searchbar menu|- Leftover Files: On first use, a warning/explanation sheet will be presented to the user", "3.5.0": "- Finder Extension: When enabled, you can right click an app in finder and uninstall with Pearcleaner directly|- Theme System: You can now select a base theme color for the application window", "3.3.0": "- Folder Settings: Add more directories where Pearcleaner should search for app files|- Progress: Show progress bar on startup when loading all app files with instant search enabled|- App Icon: Show background color behind app icons in FilesView based on icon's average color mapping|- Progress: Show time counter on regular file search views", "3.2.0": "- Menubar Item: You can choose to access Pearcleaner from the menubar. This will show a slightly modified Mini Mode view.", "3.1.0": "- Homebrew Cleanup: If you install apps using brew, you can now enable Homebrew cleanup in the settings to have Pearcleaner alert Homebrew that the app was removed externally from Homebrew and keep the brew app list synced.", "3.0.0": "- Instant Search: Enable in settings to load app files on startup|- Semantic Versioning: Going forward will use semver (Ex. v0.0.0)|-Feature Alert: For each version, a feature alert will popup once on startup to show details|- Leftover File Cleaning: Search your Mac for files leftover by uninstalled apps|- Sidebar Drag Handle: Resize the sidebar in regular mode|- Redesign UI/Settings/Icon: Some new buttons, layout changes, new official app icon and pear color theme accents|- File Sort: Sort files alphabetically or by size|- Socket File Removal: Finds socket files that aren't even visible in Finder with show hidden files enabled|- OSLog Output: Will print errors to the Console app for easier troubleshooting" } ================================================ FILE: Pearcleaner.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 73; objects = { /* Begin PBXBuildFile section */ C707E2942EAF217200AAD817 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = C707E2932EAF217200AAD817 /* Sparkle */; }; C71449E52DA826610032B295 /* Lipo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7C90DD22DA81C590056769A /* Lipo.swift */; }; C7149E792E68DE7E00A15356 /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = C7149E782E68DE7E00A15356 /* Localizable.xcstrings */; }; C72893052AFD42E600C8C1CD /* DeepLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893042AFD42E600C8C1CD /* DeepLink.swift */; }; C72893122AFD51EA00C8C1CD /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72893112AFD51EA00C8C1CD /* main.swift */; }; C72D03B52E4D3FB900ADC1B4 /* MainWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72D03B22E4D3FB900ADC1B4 /* MainWindow.swift */; }; C72FDCB92D6D089100DE7D56 /* UndoManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C72FDCB82D6D088A00DE7D56 /* UndoManager.swift */; }; C73545EB2D8496E000A3DC0C /* HelperToolManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C73545EA2D8496DA00A3DC0C /* HelperToolManager.swift */; }; C73545F92D849B5700A3DC0C /* PearcleanerHelper in CopyFiles */ = {isa = PBXBuildFile; fileRef = C73545F02D849A1B00A3DC0C /* PearcleanerHelper */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; C73545FD2D84E5DD00A3DC0C /* com.alienator88.PearcleanerSentinel.plist in CopyFiles */ = {isa = PBXBuildFile; fileRef = C7B3AB072D83900B00D829C5 /* com.alienator88.PearcleanerSentinel.plist */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; C739C3382EB027A9003639C6 /* askpass.sh in Resources */ = {isa = PBXBuildFile; fileRef = C739C3372EB027A9003639C6 /* askpass.sh */; }; C73AE1922EC69A81009CAC42 /* GlobalConsoleManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C73AE1912EC69A81009CAC42 /* GlobalConsoleManager.swift */; }; C74206E52E79E47400BDDB6A /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C74206E42E79E47400BDDB6A /* Assets.xcassets */; }; C74422412E4936750002D277 /* DaemonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74422402E4936700002D277 /* DaemonView.swift */; }; C74422432E494D830002D277 /* PackageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74422422E494D7E0002D277 /* PackageView.swift */; }; C74FDC632BCD833D00B8960F /* Conditions.swift in Sources */ = {isa = PBXBuildFile; fileRef = C74FDC622BCD833D00B8960F /* Conditions.swift */; }; C7575D392B0182DE006A600A /* PearcleanerSentinel in CopyFiles */ = {isa = PBXBuildFile; fileRef = C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; C75AED0E2EC259A900824CCD /* DeleteHistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C75AED0D2EC259A900824CCD /* DeleteHistoryView.swift */; }; C75AED102EC259C800824CCD /* UndoHistoryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C75AED0F2EC259C800824CCD /* UndoHistoryManager.swift */; }; C76EDCD62C45DC82000CF29D /* AlinFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = C76EDCD52C45DC82000CF29D /* AlinFoundation */; }; C77020292EA94D0A00240D58 /* FuzzySearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77020282EA94D0A00240D58 /* FuzzySearch.swift */; }; C770B6ED2E78DF9D005A0ABD /* Glass.icon in Resources */ = {isa = PBXBuildFile; fileRef = C770B6EC2E78DF9D005A0ABD /* Glass.icon */; }; C770D7DB2CE7C9AD00B3492A /* DevelopmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C770D7DA2CE7C9A100B3492A /* DevelopmentView.swift */; }; C7736E4F2C49A44200917293 /* AlinFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = C7736E4E2C49A44200917293 /* AlinFoundation */; }; C77B90042AF18E2E009CC655 /* PearcleanerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B90032AF18E2E009CC655 /* PearcleanerApp.swift */; }; C77B90162AF19377009CC655 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B90152AF19377009CC655 /* AppState.swift */; }; C77B90182AF19382009CC655 /* AppCommands.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B90172AF19382009CC655 /* AppCommands.swift */; }; C77B901A2AF1938F009CC655 /* Logic.swift in Sources */ = {isa = PBXBuildFile; fileRef = C77B90192AF1938F009CC655 /* Logic.swift */; }; C78121662BC892A000BE06BD /* FinderOpen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C78121652BC892A000BE06BD /* FinderOpen.swift */; }; C783F93A2E9039EB003450E7 /* PackageKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C783F9392E9039EB003450E7 /* PackageKit.framework */; }; C78733BF2E674510008615F2 /* FileWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = C78733BC2E674496008615F2 /* FileWatcher.swift */; }; C78D7B212D556F1A003DD49F /* AppPathsFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C78D7B202D556F16003DD49F /* AppPathsFetch.swift */; }; C78FF5E12EC1A3B400FC610A /* PasswordRequestHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C78FF5E02EC1A3B400FC610A /* PasswordRequestHandler.swift */; }; C78FF5E32EC1B1E100FC610A /* KeychainPasswordManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C78FF5E22EC1B1E100FC610A /* KeychainPasswordManager.swift */; }; C7A2A4D62DDB957D006E05E4 /* CLI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7A2A4D52DDB957D006E05E4 /* CLI.swift */; }; C7A2A4D92DDB95C4006E05E4 /* ArgumentParser in Frameworks */ = {isa = PBXBuildFile; productRef = C7A2A4D82DDB95C4006E05E4 /* ArgumentParser */; }; C7A6DBF12C9DD27200CFA042 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C7A6DBF02C9DD27200CFA042 /* Assets.xcassets */; }; C7BB99292E8AF1CB007BBB81 /* PluginsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7BB99282E8AF1CB007BBB81 /* PluginsView.swift */; }; C7BB992F2E8B225F007BBB81 /* FileSearchView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7BB992E2E8B225F007BBB81 /* FileSearchView.swift */; }; C7BE91A82C9E37AB009FA129 /* FinderOpen.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = C78121632BC892A000BE06BD /* FinderOpen.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; C7C90DD32DA81C5D0056769A /* Lipo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7C90DD22DA81C590056769A /* Lipo.swift */; }; C7CC10E42E94734E0057B7E2 /* ProcessEnv.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7CC10E32E9473480057B7E2 /* ProcessEnv.swift */; }; C7D31D512AFF00F300C7ED9E /* Locations.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7D31D502AFF00F300C7ED9E /* Locations.swift */; }; C7DD49EA2BAB4D8100CCBA16 /* AppInfoFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DD49E92BAB4D8100CCBA16 /* AppInfoFetch.swift */; }; C7DD49EE2BAB7F6000CCBA16 /* ReversePathsFetch.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7DD49ED2BAB7F6000CCBA16 /* ReversePathsFetch.swift */; }; C7F539382AF60865007DF1B2 /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7F539372AF60865007DF1B2 /* Utilities.swift */; }; C7F8436B2CBF066F00E3E30A /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = C7F8436A2CBF066F00E3E30A /* Localizable.xcstrings */; }; C7FBD9DF2DAD66D500151934 /* AlinFoundation in Frameworks */ = {isa = PBXBuildFile; productRef = C7C90DDD2DA8221B0056769A /* AlinFoundation */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ C78121692BC892A000BE06BD /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = C77B8FF82AF18E2E009CC655 /* Project object */; proxyType = 1; remoteGlobalIDString = C78121622BC892A000BE06BD; remoteInfo = FinderOpen; }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ C728930D2AFD51EA00C8C1CD /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = /usr/share/man/man1/; dstSubfolderSpec = 0; files = ( ); runOnlyForDeploymentPostprocessing = 1; }; C73545EE2D849A1B00A3DC0C /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = /usr/share/man/man1/; dstSubfolderSpec = 0; files = ( ); runOnlyForDeploymentPostprocessing = 1; }; C73545FA2D849B6900A3DC0C /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = Contents/Library/LaunchDaemons; dstSubfolderSpec = 1; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; C781216C2BC892A000BE06BD /* Embed Foundation Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 13; files = ( C7BE91A82C9E37AB009FA129 /* FinderOpen.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; }; C7A27E7D2AFD65C400166168 /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 6; files = ( C73545F92D849B5700A3DC0C /* PearcleanerHelper in CopyFiles */, C7575D392B0182DE006A600A /* PearcleanerSentinel in CopyFiles */, ); runOnlyForDeploymentPostprocessing = 0; }; C7B3AB0B2D83910400D829C5 /* CopyFiles */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = Contents/Library/LaunchAgents; dstSubfolderSpec = 1; files = ( C73545FD2D84E5DD00A3DC0C /* com.alienator88.PearcleanerSentinel.plist in CopyFiles */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ C7149E782E68DE7E00A15356 /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; C72893042AFD42E600C8C1CD /* DeepLink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeepLink.swift; sourceTree = ""; }; C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = PearcleanerSentinel; sourceTree = BUILT_PRODUCTS_DIR; }; C72893112AFD51EA00C8C1CD /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; C72D03B22E4D3FB900ADC1B4 /* MainWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainWindow.swift; sourceTree = ""; }; C72FDCB82D6D088A00DE7D56 /* UndoManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UndoManager.swift; sourceTree = ""; }; C73545EA2D8496DA00A3DC0C /* HelperToolManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HelperToolManager.swift; sourceTree = ""; }; C73545F02D849A1B00A3DC0C /* PearcleanerHelper */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = PearcleanerHelper; sourceTree = BUILT_PRODUCTS_DIR; }; C739C3372EB027A9003639C6 /* askpass.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = askpass.sh; sourceTree = ""; }; C73AE1912EC69A81009CAC42 /* GlobalConsoleManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalConsoleManager.swift; sourceTree = ""; }; C74206E42E79E47400BDDB6A /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; C74422402E4936700002D277 /* DaemonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DaemonView.swift; sourceTree = ""; }; C74422422E494D7E0002D277 /* PackageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PackageView.swift; sourceTree = ""; }; C74451F62D93774700146AF8 /* announcements.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = announcements.json; sourceTree = ""; }; C74FDC622BCD833D00B8960F /* Conditions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Conditions.swift; sourceTree = ""; }; C75AED0D2EC259A900824CCD /* DeleteHistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteHistoryView.swift; sourceTree = ""; }; C75AED0F2EC259C800824CCD /* UndoHistoryManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UndoHistoryManager.swift; sourceTree = ""; }; C77020282EA94D0A00240D58 /* FuzzySearch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FuzzySearch.swift; sourceTree = ""; }; C770B6EC2E78DF9D005A0ABD /* Glass.icon */ = {isa = PBXFileReference; lastKnownFileType = folder.iconcomposer.icon; path = Glass.icon; sourceTree = ""; }; C770D7DA2CE7C9A100B3492A /* DevelopmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DevelopmentView.swift; sourceTree = ""; }; C77B90002AF18E2E009CC655 /* Pearcleaner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Pearcleaner.app; sourceTree = BUILT_PRODUCTS_DIR; }; C77B90032AF18E2E009CC655 /* PearcleanerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PearcleanerApp.swift; sourceTree = ""; }; C77B90142AF18E7C009CC655 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; C77B90152AF19377009CC655 /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = ""; }; C77B90172AF19382009CC655 /* AppCommands.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCommands.swift; sourceTree = ""; }; C77B90192AF1938F009CC655 /* Logic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logic.swift; sourceTree = ""; }; C77B90242AF2D796009CC655 /* Pearcleaner.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Pearcleaner.entitlements; sourceTree = ""; }; C78121632BC892A000BE06BD /* FinderOpen.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = FinderOpen.appex; sourceTree = BUILT_PRODUCTS_DIR; }; C78121652BC892A000BE06BD /* FinderOpen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FinderOpen.swift; sourceTree = ""; }; C78121672BC892A000BE06BD /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; C78121682BC892A000BE06BD /* FinderOpen.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = FinderOpen.entitlements; sourceTree = ""; }; C783F9392E9039EB003450E7 /* PackageKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PackageKit.framework; path = ../../../../System/Library/PrivateFrameworks/PackageKit.framework; sourceTree = ""; }; C786F1A72C29E599001B3800 /* Builds */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Builds; sourceTree = SOURCE_ROOT; }; C78733BC2E674496008615F2 /* FileWatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileWatcher.swift; sourceTree = ""; }; C78D7B202D556F16003DD49F /* AppPathsFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPathsFetch.swift; sourceTree = ""; }; C78FF5E02EC1A3B400FC610A /* PasswordRequestHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordRequestHandler.swift; sourceTree = ""; }; C78FF5E22EC1B1E100FC610A /* KeychainPasswordManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainPasswordManager.swift; sourceTree = ""; }; C7A2A4D52DDB957D006E05E4 /* CLI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CLI.swift; sourceTree = ""; }; C7A6DBF02C9DD27200CFA042 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; C7B3AB072D83900B00D829C5 /* com.alienator88.PearcleanerSentinel.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = com.alienator88.PearcleanerSentinel.plist; sourceTree = ""; }; C7BB99282E8AF1CB007BBB81 /* PluginsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginsView.swift; sourceTree = ""; }; C7BB992E2E8B225F007BBB81 /* FileSearchView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileSearchView.swift; sourceTree = ""; }; C7C90DD22DA81C590056769A /* Lipo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Lipo.swift; sourceTree = ""; }; C7CC10E32E9473480057B7E2 /* ProcessEnv.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessEnv.swift; sourceTree = ""; }; C7D31D502AFF00F300C7ED9E /* Locations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Locations.swift; sourceTree = ""; }; C7DD49E92BAB4D8100CCBA16 /* AppInfoFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoFetch.swift; sourceTree = ""; }; C7DD49ED2BAB7F6000CCBA16 /* ReversePathsFetch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReversePathsFetch.swift; sourceTree = ""; }; C7F539372AF60865007DF1B2 /* Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = ""; }; C7F8436A2CBF066F00E3E30A /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ C74649F02E480128002D1C63 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( ChromeBorder.swift, CircularProgressView.swift, ControlGroupChrome.swift, PearGroupBox.swift, SparkleProgressBar.swift, Styles.swift, Theme.swift, ); target = C77B8FFF2AF18E2E009CC655 /* Pearcleaner */; }; C75E82E12E8C585D0090C3B1 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( AppGroupDefaults.swift, ); target = C73545EF2D849A1B00A3DC0C /* PearcleanerHelper */; }; C75E82E42E8C58620090C3B1 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( AppGroupDefaults.swift, ); target = C77B8FFF2AF18E2E009CC655 /* Pearcleaner */; }; C75E82E52E8C58620090C3B1 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( AppGroupDefaults.swift, ); target = C78121622BC892A000BE06BD /* FinderOpen */; }; /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet section */ C73545FC2D849B7E00A3DC0C /* PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet */ = { isa = PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet; attributesByRelativePath = { com.alienator88.Pearcleaner.PearcleanerHelper.plist = ( CodeSignOnCopy, ); }; buildPhase = C73545FA2D849B6900A3DC0C /* CopyFiles */; membershipExceptions = ( com.alienator88.Pearcleaner.PearcleanerHelper.plist, ); }; /* End PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ C71B87562EF49C58009C5B07 /* TCC */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = TCC; sourceTree = ""; }; C71DC3392EAAAFF000503474 /* Components */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Components; sourceTree = ""; }; C73545F12D849A1B00A3DC0C /* PearcleanerHelper */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (C73545FC2D849B7E00A3DC0C /* PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = PearcleanerHelper; sourceTree = ""; }; C74649E62E48000C002D1C63 /* LipoView */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = LipoView; sourceTree = ""; }; C74649EA2E48008A002D1C63 /* ZombieView */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = ZombieView; sourceTree = ""; }; C74649EE2E4800BB002D1C63 /* AppsView */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = AppsView; sourceTree = ""; }; C74649EF2E480111002D1C63 /* Style */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (C74649F02E480128002D1C63 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Style; sourceTree = ""; }; C74649FA2E48076B002D1C63 /* Settings */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Settings; sourceTree = ""; }; C75E82DD2E8C58380090C3B1 /* Shared */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (C75E82E42E8C58620090C3B1 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, C75E82E52E8C58620090C3B1 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, C75E82E12E8C585D0090C3B1 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = Shared; sourceTree = ""; }; C76073F32E3C1C2C003957A7 /* FilesView */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = FilesView; sourceTree = ""; }; C783F8FD2E903707003450E7 /* FileSearch */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = FileSearch; sourceTree = ""; }; C783F9492E903B53003450E7 /* PKG */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = PKG; sourceTree = ""; }; C7923DCC2E90807F005C5632 /* Brew */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Brew; sourceTree = ""; }; C7C05FD72E8DA12C006D0476 /* Brew */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = Brew; sourceTree = ""; }; C7C693E92E9D73DD0081CF25 /* AppsUpdater */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = AppsUpdater; sourceTree = ""; }; C7C693FC2E9D74D50081CF25 /* AppsUpdaterView */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = AppsUpdaterView; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ C728930C2AFD51EA00C8C1CD /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; C73545ED2D849A1B00A3DC0C /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( C7FBD9DF2DAD66D500151934 /* AlinFoundation in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; C77B8FFD2AF18E2E009CC655 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( C707E2942EAF217200AAD817 /* Sparkle in Frameworks */, C76EDCD62C45DC82000CF29D /* AlinFoundation in Frameworks */, C7A2A4D92DDB95C4006E05E4 /* ArgumentParser in Frameworks */, C783F93A2E9039EB003450E7 /* PackageKit.framework in Frameworks */, C7736E4F2C49A44200917293 /* AlinFoundation in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; C78121602BC892A000BE06BD /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ C72893072AFD442600C8C1CD /* Logic */ = { isa = PBXGroup; children = ( C77B90172AF19382009CC655 /* AppCommands.swift */, C7DD49E92BAB4D8100CCBA16 /* AppInfoFetch.swift */, C78D7B202D556F16003DD49F /* AppPathsFetch.swift */, C77B90152AF19377009CC655 /* AppState.swift */, C7A2A4D52DDB957D006E05E4 /* CLI.swift */, C74FDC622BCD833D00B8960F /* Conditions.swift */, C72893042AFD42E600C8C1CD /* DeepLink.swift */, C77020282EA94D0A00240D58 /* FuzzySearch.swift */, C73AE1912EC69A81009CAC42 /* GlobalConsoleManager.swift */, C73545EA2D8496DA00A3DC0C /* HelperToolManager.swift */, C78FF5E22EC1B1E100FC610A /* KeychainPasswordManager.swift */, C7C90DD22DA81C590056769A /* Lipo.swift */, C7D31D502AFF00F300C7ED9E /* Locations.swift */, C77B90192AF1938F009CC655 /* Logic.swift */, C78FF5E02EC1A3B400FC610A /* PasswordRequestHandler.swift */, C7CC10E32E9473480057B7E2 /* ProcessEnv.swift */, C7DD49ED2BAB7F6000CCBA16 /* ReversePathsFetch.swift */, C75AED0F2EC259C800824CCD /* UndoHistoryManager.swift */, C72FDCB82D6D088A00DE7D56 /* UndoManager.swift */, C7F539372AF60865007DF1B2 /* Utilities.swift */, C7C693E92E9D73DD0081CF25 /* AppsUpdater */, C7923DCC2E90807F005C5632 /* Brew */, C783F8FD2E903707003450E7 /* FileSearch */, C783F9492E903B53003450E7 /* PKG */, C71B87562EF49C58009C5B07 /* TCC */, ); path = Logic; sourceTree = ""; }; C72893102AFD51EA00C8C1CD /* PearcleanerSentinel */ = { isa = PBXGroup; children = ( C7B3AB072D83900B00D829C5 /* com.alienator88.PearcleanerSentinel.plist */, C78733BC2E674496008615F2 /* FileWatcher.swift */, C72893112AFD51EA00C8C1CD /* main.swift */, ); path = PearcleanerSentinel; sourceTree = ""; }; C72893162AFD531500C8C1CD /* Frameworks */ = { isa = PBXGroup; children = ( C783F9392E9039EB003450E7 /* PackageKit.framework */, ); name = Frameworks; sourceTree = ""; }; C76D084F2AF84E0100D07867 /* Resources */ = { isa = PBXGroup; children = ( C77B90142AF18E7C009CC655 /* Info.plist */, C77B90242AF2D796009CC655 /* Pearcleaner.entitlements */, C770B6EC2E78DF9D005A0ABD /* Glass.icon */, C74206E42E79E47400BDDB6A /* Assets.xcassets */, C7F8436A2CBF066F00E3E30A /* Localizable.xcstrings */, C739C3372EB027A9003639C6 /* askpass.sh */, ); path = Resources; sourceTree = ""; }; C77B8FF72AF18E2E009CC655 = { isa = PBXGroup; children = ( C75E82DD2E8C58380090C3B1 /* Shared */, C77B90022AF18E2E009CC655 /* Pearcleaner */, C72893102AFD51EA00C8C1CD /* PearcleanerSentinel */, C78121642BC892A000BE06BD /* FinderOpen */, C73545F12D849A1B00A3DC0C /* PearcleanerHelper */, C77B90012AF18E2E009CC655 /* Products */, C72893162AFD531500C8C1CD /* Frameworks */, ); sourceTree = ""; }; C77B90012AF18E2E009CC655 /* Products */ = { isa = PBXGroup; children = ( C77B90002AF18E2E009CC655 /* Pearcleaner.app */, C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */, C78121632BC892A000BE06BD /* FinderOpen.appex */, C73545F02D849A1B00A3DC0C /* PearcleanerHelper */, ); name = Products; sourceTree = ""; }; C77B90022AF18E2E009CC655 /* Pearcleaner */ = { isa = PBXGroup; children = ( C77B90032AF18E2E009CC655 /* PearcleanerApp.swift */, C74451F62D93774700146AF8 /* announcements.json */, C786F1A72C29E599001B3800 /* Builds */, C76D084F2AF84E0100D07867 /* Resources */, C77B901F2AF1B390009CC655 /* Views */, C74649EF2E480111002D1C63 /* Style */, C72893072AFD442600C8C1CD /* Logic */, ); path = Pearcleaner; sourceTree = ""; }; C77B901F2AF1B390009CC655 /* Views */ = { isa = PBXGroup; children = ( C74422402E4936700002D277 /* DaemonView.swift */, C75AED0D2EC259A900824CCD /* DeleteHistoryView.swift */, C770D7DA2CE7C9A100B3492A /* DevelopmentView.swift */, C7BB992E2E8B225F007BBB81 /* FileSearchView.swift */, C72D03B22E4D3FB900ADC1B4 /* MainWindow.swift */, C74422422E494D7E0002D277 /* PackageView.swift */, C7BB99282E8AF1CB007BBB81 /* PluginsView.swift */, C7C693FC2E9D74D50081CF25 /* AppsUpdaterView */, C74649EE2E4800BB002D1C63 /* AppsView */, C7C05FD72E8DA12C006D0476 /* Brew */, C71DC3392EAAAFF000503474 /* Components */, C76073F32E3C1C2C003957A7 /* FilesView */, C74649E62E48000C002D1C63 /* LipoView */, C74649FA2E48076B002D1C63 /* Settings */, C74649EA2E48008A002D1C63 /* ZombieView */, ); path = Views; sourceTree = ""; }; C78121642BC892A000BE06BD /* FinderOpen */ = { isa = PBXGroup; children = ( C78121652BC892A000BE06BD /* FinderOpen.swift */, C78121672BC892A000BE06BD /* Info.plist */, C78121682BC892A000BE06BD /* FinderOpen.entitlements */, C7A6DBF02C9DD27200CFA042 /* Assets.xcassets */, C7149E782E68DE7E00A15356 /* Localizable.xcstrings */, ); path = FinderOpen; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ C728930E2AFD51EA00C8C1CD /* PearcleanerSentinel */ = { isa = PBXNativeTarget; buildConfigurationList = C72893132AFD51EA00C8C1CD /* Build configuration list for PBXNativeTarget "PearcleanerSentinel" */; buildPhases = ( C728930B2AFD51EA00C8C1CD /* Sources */, C728930C2AFD51EA00C8C1CD /* Frameworks */, C728930D2AFD51EA00C8C1CD /* CopyFiles */, ); buildRules = ( ); dependencies = ( ); name = PearcleanerSentinel; packageProductDependencies = ( ); productName = PeakPurgeMonitor; productReference = C728930F2AFD51EA00C8C1CD /* PearcleanerSentinel */; productType = "com.apple.product-type.tool"; }; C73545EF2D849A1B00A3DC0C /* PearcleanerHelper */ = { isa = PBXNativeTarget; buildConfigurationList = C73545F42D849A1B00A3DC0C /* Build configuration list for PBXNativeTarget "PearcleanerHelper" */; buildPhases = ( C73545EC2D849A1B00A3DC0C /* Sources */, C73545ED2D849A1B00A3DC0C /* Frameworks */, C73545EE2D849A1B00A3DC0C /* CopyFiles */, ); buildRules = ( ); dependencies = ( ); fileSystemSynchronizedGroups = ( C73545F12D849A1B00A3DC0C /* PearcleanerHelper */, C75E82DD2E8C58380090C3B1 /* Shared */, ); name = PearcleanerHelper; packageProductDependencies = ( C7C90DDD2DA8221B0056769A /* AlinFoundation */, ); productName = PearcleanerHelper; productReference = C73545F02D849A1B00A3DC0C /* PearcleanerHelper */; productType = "com.apple.product-type.tool"; }; C77B8FFF2AF18E2E009CC655 /* Pearcleaner */ = { isa = PBXNativeTarget; buildConfigurationList = C77B900F2AF18E2F009CC655 /* Build configuration list for PBXNativeTarget "Pearcleaner" */; buildPhases = ( C77B8FFC2AF18E2E009CC655 /* Sources */, C77B8FFD2AF18E2E009CC655 /* Frameworks */, C77B8FFE2AF18E2E009CC655 /* Resources */, C7A27E7D2AFD65C400166168 /* CopyFiles */, C781216C2BC892A000BE06BD /* Embed Foundation Extensions */, C7B3AB0B2D83910400D829C5 /* CopyFiles */, C73545FA2D849B6900A3DC0C /* CopyFiles */, C739C33B2EB0280F003639C6 /* ShellScript */, ); buildRules = ( ); dependencies = ( C781216A2BC892A000BE06BD /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( C71B87562EF49C58009C5B07 /* TCC */, C71DC3392EAAAFF000503474 /* Components */, C74649E62E48000C002D1C63 /* LipoView */, C74649EA2E48008A002D1C63 /* ZombieView */, C74649EE2E4800BB002D1C63 /* AppsView */, C74649FA2E48076B002D1C63 /* Settings */, C76073F32E3C1C2C003957A7 /* FilesView */, C783F8FD2E903707003450E7 /* FileSearch */, C783F9492E903B53003450E7 /* PKG */, C7923DCC2E90807F005C5632 /* Brew */, C7C05FD72E8DA12C006D0476 /* Brew */, C7C693E92E9D73DD0081CF25 /* AppsUpdater */, C7C693FC2E9D74D50081CF25 /* AppsUpdaterView */, ); name = Pearcleaner; packageProductDependencies = ( C76EDCD52C45DC82000CF29D /* AlinFoundation */, C7736E4E2C49A44200917293 /* AlinFoundation */, C7A2A4D82DDB95C4006E05E4 /* ArgumentParser */, C707E2932EAF217200AAD817 /* Sparkle */, ); productName = PeakPurge; productReference = C77B90002AF18E2E009CC655 /* Pearcleaner.app */; productType = "com.apple.product-type.application"; }; C78121622BC892A000BE06BD /* FinderOpen */ = { isa = PBXNativeTarget; buildConfigurationList = C781216F2BC892A000BE06BD /* Build configuration list for PBXNativeTarget "FinderOpen" */; buildPhases = ( C781215F2BC892A000BE06BD /* Sources */, C78121602BC892A000BE06BD /* Frameworks */, C78121612BC892A000BE06BD /* Resources */, ); buildRules = ( ); dependencies = ( ); name = FinderOpen; productName = FinderOpen; productReference = C78121632BC892A000BE06BD /* FinderOpen.appex */; productType = "com.apple.product-type.app-extension"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ C77B8FF82AF18E2E009CC655 /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1620; LastUpgradeCheck = 2600; TargetAttributes = { C728930E2AFD51EA00C8C1CD = { CreatedOnToolsVersion = 15.0.1; }; C73545EF2D849A1B00A3DC0C = { CreatedOnToolsVersion = 16.2; LastSwiftMigration = 1640; }; C77B8FFF2AF18E2E009CC655 = { CreatedOnToolsVersion = 15.0.1; }; C78121622BC892A000BE06BD = { CreatedOnToolsVersion = 15.3; }; }; }; buildConfigurationList = C77B8FFB2AF18E2E009CC655 /* Build configuration list for PBXProject "Pearcleaner" */; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, sk, "zh-HK", Base, "zh-Hant", ru, de, fr, "zh-Hans", ja, tr, es, uk, vi, "pt-BR", id, pl, sl, it, ko, "pt-PT", ); mainGroup = C77B8FF72AF18E2E009CC655; packageReferences = ( C7736E4D2C49A44200917293 /* XCRemoteSwiftPackageReference "AlinFoundation" */, C7A2A4D72DDB95C4006E05E4 /* XCRemoteSwiftPackageReference "swift-argument-parser" */, C707E2922EAF217200AAD817 /* XCRemoteSwiftPackageReference "Sparkle" */, ); preferredProjectObjectVersion = 56; productRefGroup = C77B90012AF18E2E009CC655 /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( C77B8FFF2AF18E2E009CC655 /* Pearcleaner */, C728930E2AFD51EA00C8C1CD /* PearcleanerSentinel */, C78121622BC892A000BE06BD /* FinderOpen */, C73545EF2D849A1B00A3DC0C /* PearcleanerHelper */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ C77B8FFE2AF18E2E009CC655 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( C770B6ED2E78DF9D005A0ABD /* Glass.icon in Resources */, C739C3382EB027A9003639C6 /* askpass.sh in Resources */, C74206E52E79E47400BDDB6A /* Assets.xcassets in Resources */, C7F8436B2CBF066F00E3E30A /* Localizable.xcstrings in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; C78121612BC892A000BE06BD /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( C7149E792E68DE7E00A15356 /* Localizable.xcstrings in Resources */, C7A6DBF12C9DD27200CFA042 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ C739C33B2EB0280F003639C6 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( ); outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/bash; shellScript = "chmod +x \"${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app/Contents/Resources/askpass.sh\"\n"; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ C728930B2AFD51EA00C8C1CD /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( C78733BF2E674510008615F2 /* FileWatcher.swift in Sources */, C72893122AFD51EA00C8C1CD /* main.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; C73545EC2D849A1B00A3DC0C /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( C71449E52DA826610032B295 /* Lipo.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; C77B8FFC2AF18E2E009CC655 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( C78FF5E12EC1A3B400FC610A /* PasswordRequestHandler.swift in Sources */, C7DD49EA2BAB4D8100CCBA16 /* AppInfoFetch.swift in Sources */, C75AED102EC259C800824CCD /* UndoHistoryManager.swift in Sources */, C7CC10E42E94734E0057B7E2 /* ProcessEnv.swift in Sources */, C74422432E494D830002D277 /* PackageView.swift in Sources */, C77B90162AF19377009CC655 /* AppState.swift in Sources */, C7DD49EE2BAB7F6000CCBA16 /* ReversePathsFetch.swift in Sources */, C72D03B52E4D3FB900ADC1B4 /* MainWindow.swift in Sources */, C77020292EA94D0A00240D58 /* FuzzySearch.swift in Sources */, C7BB99292E8AF1CB007BBB81 /* PluginsView.swift in Sources */, C7C90DD32DA81C5D0056769A /* Lipo.swift in Sources */, C78FF5E32EC1B1E100FC610A /* KeychainPasswordManager.swift in Sources */, C7A2A4D62DDB957D006E05E4 /* CLI.swift in Sources */, C7D31D512AFF00F300C7ED9E /* Locations.swift in Sources */, C77B901A2AF1938F009CC655 /* Logic.swift in Sources */, C78D7B212D556F1A003DD49F /* AppPathsFetch.swift in Sources */, C770D7DB2CE7C9AD00B3492A /* DevelopmentView.swift in Sources */, C74422412E4936750002D277 /* DaemonView.swift in Sources */, C72893052AFD42E600C8C1CD /* DeepLink.swift in Sources */, C73545EB2D8496E000A3DC0C /* HelperToolManager.swift in Sources */, C72FDCB92D6D089100DE7D56 /* UndoManager.swift in Sources */, C7BB992F2E8B225F007BBB81 /* FileSearchView.swift in Sources */, C75AED0E2EC259A900824CCD /* DeleteHistoryView.swift in Sources */, C73AE1922EC69A81009CAC42 /* GlobalConsoleManager.swift in Sources */, C77B90042AF18E2E009CC655 /* PearcleanerApp.swift in Sources */, C77B90182AF19382009CC655 /* AppCommands.swift in Sources */, C74FDC632BCD833D00B8960F /* Conditions.swift in Sources */, C7F539382AF60865007DF1B2 /* Utilities.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; C781215F2BC892A000BE06BD /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( C78121662BC892A000BE06BD /* FinderOpen.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ C781216A2BC892A000BE06BD /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = C78121622BC892A000BE06BD /* FinderOpen */; targetProxy = C78121692BC892A000BE06BD /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ C72893142AFD51EA00C8C1CD /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { APP_BUILD = "$(APP_BUILD)"; APP_VERSION = "$(APP_VERSION)"; CODE_SIGN_ENTITLEMENTS = ""; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = "$(APP_BUILD)"; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=macosx*]" = BK8443AXLU; ENABLE_HARDENED_RUNTIME = YES; FRAMEWORK_SEARCH_PATHS = ""; INSTALL_PATH = ""; MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = "$(APP_VERSION)"; PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.PearcleanerSentinel; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; }; name = Debug; }; C72893152AFD51EA00C8C1CD /* Release */ = { isa = XCBuildConfiguration; buildSettings = { APP_BUILD = "$(APP_BUILD)"; APP_VERSION = "$(APP_VERSION)"; CODE_SIGN_ENTITLEMENTS = ""; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = "$(APP_BUILD)"; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=macosx*]" = BK8443AXLU; ENABLE_HARDENED_RUNTIME = YES; FRAMEWORK_SEARCH_PATHS = ""; INSTALL_PATH = ""; MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = "$(APP_VERSION)"; PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.PearcleanerSentinel; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; }; name = Release; }; C73545F52D849A1B00A3DC0C /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = "$(APP_BUILD)"; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=macosx*]" = BK8443AXLU; ENABLE_HARDENED_RUNTIME = YES; INSTALL_PATH = ""; MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = "$(APP_VERSION)"; PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.Pearcleaner.PearcleanerHelper; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; }; name = Debug; }; C73545F62D849A1B00A3DC0C /* Release */ = { isa = XCBuildConfiguration; buildSettings = { CLANG_ENABLE_MODULES = YES; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = "$(APP_BUILD)"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=macosx*]" = BK8443AXLU; ENABLE_HARDENED_RUNTIME = YES; INSTALL_PATH = ""; MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = "$(APP_VERSION)"; PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.Pearcleaner.PearcleanerHelper; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_VERSION = 5.0; }; name = Release; }; C77B900D2AF18E2F009CC655 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; APP_BUILD = 121; APP_VERSION = 5.4.3; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = ""; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 77; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; FRAMEWORK_SEARCH_PATHS = ""; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 4.4.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; C77B900E2AF18E2F009CC655 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; APP_BUILD = 121; APP_VERSION = 5.4.3; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_ENTITLEMENTS = ""; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 77; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; FRAMEWORK_SEARCH_PATHS = ""; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = 4.4.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OPTIMIZATION_LEVEL = "-O"; }; name = Release; }; C77B90102AF18E2F009CC655 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { APP_BUILD = "$(APP_BUILD)"; APP_VERSION = "$(APP_VERSION)"; ASSETCATALOG_COMPILER_APPICON_NAME = Glass; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_OPTIMIZATION = time; ASSETCATALOG_COMPILER_SKIP_APP_STORE_DEPLOYMENT = YES; ASSETCATALOG_COMPILER_STANDALONE_ICON_BEHAVIOR = default; CODE_SIGN_ENTITLEMENTS = Pearcleaner/Resources/Pearcleaner.entitlements; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = "$(APP_BUILD)"; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=macosx*]" = BK8443AXLU; ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)", ); GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Pearcleaner/Resources/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Pearcleaner; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "Pearcleaner requires full disk access to be able to delete files in locations with no permission"; INFOPLIST_KEY_NSSystemExtensionUsageDescription = "Pearcleaner requires this permission for the Finder Extension"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = "$(APP_VERSION)"; ONLY_ACTIVE_ARCH = YES; OTHER_LDFLAGS = ""; OTHER_SWIFT_FLAGS = "-Xcc -fmodule-map-file=/Users/alin/GitHub/PearcleanerGH/Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/CommerceKit/module.modulemap -Xcc -fmodule-map-file=/Users/alin/GitHub/PearcleanerGH/Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/StoreFoundation/module.modulemap"; PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.Pearcleaner; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "M1 Pro Signed - Pearcleaner"; REGISTER_APP_GROUPS = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "Pearcleaner/Logic/PKG/Pearcleaner-Bridging-Header.h"; SWIFT_VERSION = 5.0; SYSTEM_FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks", ); }; name = Debug; }; C77B90112AF18E2F009CC655 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { APP_BUILD = "$(APP_BUILD)"; APP_VERSION = "$(APP_VERSION)"; ASSETCATALOG_COMPILER_APPICON_NAME = Glass; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_OPTIMIZATION = space; ASSETCATALOG_COMPILER_SKIP_APP_STORE_DEPLOYMENT = YES; ASSETCATALOG_COMPILER_STANDALONE_ICON_BEHAVIOR = default; CODE_SIGN_ENTITLEMENTS = Pearcleaner/Resources/Pearcleaner.entitlements; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = "$(APP_BUILD)"; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=macosx*]" = BK8443AXLU; ENABLE_APP_SANDBOX = NO; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(PROJECT_DIR)", ); GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Pearcleaner/Resources/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Pearcleaner; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSSystemAdministrationUsageDescription = "Pearcleaner requires full disk access to be able to delete files in locations with no permission"; INFOPLIST_KEY_NSSystemExtensionUsageDescription = "Pearcleaner requires this permission for the Finder Extension"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = "$(APP_VERSION)"; OTHER_LDFLAGS = ""; OTHER_SWIFT_FLAGS = "-Xcc -fmodule-map-file=/Users/alin/GitHub/PearcleanerGH/Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/CommerceKit/module.modulemap -Xcc -fmodule-map-file=/Users/alin/GitHub/PearcleanerGH/Pearcleaner/Logic/AppsUpdater/PrivateFrameworks/StoreFoundation/module.modulemap"; PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.Pearcleaner; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "M1 Pro Signed - Pearcleaner"; REGISTER_APP_GROUPS = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "Pearcleaner/Logic/PKG/Pearcleaner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Osize"; SWIFT_VERSION = 5.0; SYSTEM_FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", "$(SYSTEM_LIBRARY_DIR)/PrivateFrameworks", ); }; name = Release; }; C781216D2BC892A000BE06BD /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { APP_BUILD = "$(APP_BUILD)"; APP_VERSION = "$(APP_VERSION)"; CODE_SIGN_ENTITLEMENTS = FinderOpen/FinderOpen.entitlements; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = "$(APP_BUILD)"; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=macosx*]" = BK8443AXLU; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = FinderOpen/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = FinderOpen; INFOPLIST_KEY_LSApplicationCategoryType = ""; INFOPLIST_KEY_LSUIElement = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = "$(APP_VERSION)"; PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.Pearcleaner.FinderOpen; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "Pearcleaner Finder Extension"; REGISTER_APP_GROUPS = YES; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; name = Debug; }; C781216E2BC892A000BE06BD /* Release */ = { isa = XCBuildConfiguration; buildSettings = { APP_BUILD = "$(APP_BUILD)"; APP_VERSION = "$(APP_VERSION)"; CODE_SIGN_ENTITLEMENTS = FinderOpen/FinderOpen.entitlements; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Developer ID Application"; CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = "$(APP_BUILD)"; DEVELOPMENT_TEAM = ""; "DEVELOPMENT_TEAM[sdk=macosx*]" = BK8443AXLU; ENABLE_HARDENED_RUNTIME = YES; GCC_OPTIMIZATION_LEVEL = ""; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = FinderOpen/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = FinderOpen; INFOPLIST_KEY_LSApplicationCategoryType = ""; INFOPLIST_KEY_LSUIElement = YES; INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; MARKETING_VERSION = "$(APP_VERSION)"; PRODUCT_BUNDLE_IDENTIFIER = com.alienator88.Pearcleaner.FinderOpen; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; "PROVISIONING_PROFILE_SPECIFIER[sdk=macosx*]" = "Pearcleaner Finder Extension"; REGISTER_APP_GROUPS = YES; SKIP_INSTALL = YES; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ C72893132AFD51EA00C8C1CD /* Build configuration list for PBXNativeTarget "PearcleanerSentinel" */ = { isa = XCConfigurationList; buildConfigurations = ( C72893142AFD51EA00C8C1CD /* Debug */, C72893152AFD51EA00C8C1CD /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; C73545F42D849A1B00A3DC0C /* Build configuration list for PBXNativeTarget "PearcleanerHelper" */ = { isa = XCConfigurationList; buildConfigurations = ( C73545F52D849A1B00A3DC0C /* Debug */, C73545F62D849A1B00A3DC0C /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; C77B8FFB2AF18E2E009CC655 /* Build configuration list for PBXProject "Pearcleaner" */ = { isa = XCConfigurationList; buildConfigurations = ( C77B900D2AF18E2F009CC655 /* Debug */, C77B900E2AF18E2F009CC655 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; C77B900F2AF18E2F009CC655 /* Build configuration list for PBXNativeTarget "Pearcleaner" */ = { isa = XCConfigurationList; buildConfigurations = ( C77B90102AF18E2F009CC655 /* Debug */, C77B90112AF18E2F009CC655 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; C781216F2BC892A000BE06BD /* Build configuration list for PBXNativeTarget "FinderOpen" */ = { isa = XCConfigurationList; buildConfigurations = ( C781216D2BC892A000BE06BD /* Debug */, C781216E2BC892A000BE06BD /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ C707E2922EAF217200AAD817 /* XCRemoteSwiftPackageReference "Sparkle" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/sparkle-project/Sparkle"; requirement = { kind = upToNextMajorVersion; minimumVersion = 2.8.0; }; }; C7736E4D2C49A44200917293 /* XCRemoteSwiftPackageReference "AlinFoundation" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/alienator88/AlinFoundation"; requirement = { branch = main; kind = branch; }; }; C7A2A4D72DDB95C4006E05E4 /* XCRemoteSwiftPackageReference "swift-argument-parser" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/apple/swift-argument-parser.git"; requirement = { kind = upToNextMajorVersion; minimumVersion = 1.5.0; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ C707E2932EAF217200AAD817 /* Sparkle */ = { isa = XCSwiftPackageProductDependency; package = C707E2922EAF217200AAD817 /* XCRemoteSwiftPackageReference "Sparkle" */; productName = Sparkle; }; C76EDCD52C45DC82000CF29D /* AlinFoundation */ = { isa = XCSwiftPackageProductDependency; productName = AlinFoundation; }; C7736E4E2C49A44200917293 /* AlinFoundation */ = { isa = XCSwiftPackageProductDependency; package = C7736E4D2C49A44200917293 /* XCRemoteSwiftPackageReference "AlinFoundation" */; productName = AlinFoundation; }; C7A2A4D82DDB95C4006E05E4 /* ArgumentParser */ = { isa = XCSwiftPackageProductDependency; package = C7A2A4D72DDB95C4006E05E4 /* XCRemoteSwiftPackageReference "swift-argument-parser" */; productName = ArgumentParser; }; C7C90DDD2DA8221B0056769A /* AlinFoundation */ = { isa = XCSwiftPackageProductDependency; package = C7736E4D2C49A44200917293 /* XCRemoteSwiftPackageReference "AlinFoundation" */; productName = AlinFoundation; }; /* End XCSwiftPackageProductDependency section */ }; rootObject = C77B8FF82AF18E2E009CC655 /* Project object */; } ================================================ FILE: Pearcleaner.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: Pearcleaner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist ================================================ IDEDidComputeMac32BitWarning ================================================ FILE: Pearcleaner.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved ================================================ { "originHash" : "878acce7c95e9774ee43b058d63510f4ad7a7a3a673f5600c415bcc68aad7c3f", "pins" : [ { "identity" : "alinfoundation", "kind" : "remoteSourceControl", "location" : "https://github.com/alienator88/AlinFoundation", "state" : { "branch" : "main", "revision" : "f61241c2ea1856ef41cbfc965afe9d756121456f" } }, { "identity" : "sparkle", "kind" : "remoteSourceControl", "location" : "https://github.com/sparkle-project/Sparkle", "state" : { "revision" : "9a1d2a19d3595fcf8d9c447173f9a1687b3dcadb", "version" : "2.8.0" } }, { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { "revision" : "309a47b2b1d9b5e991f36961c983ecec72275be3", "version" : "1.6.1" } } ], "version" : 3 } ================================================ FILE: Pearcleaner.xcodeproj/xcshareddata/xcschemes/FinderOpen.xcscheme ================================================ ================================================ FILE: Pearcleaner.xcodeproj/xcshareddata/xcschemes/Pearcleaner Debug.xcscheme ================================================ ================================================ FILE: Pearcleaner.xcodeproj/xcshareddata/xcschemes/Pearcleaner Release.xcscheme ================================================ ================================================ FILE: Pearcleaner.xcodeproj/xcshareddata/xcschemes/PearcleanerSentinel Release.xcscheme ================================================ ================================================ FILE: Pearcleaner.xcodeproj/xcshareddata/xcschemes/PearcleanerSentinel.xcscheme ================================================ ================================================ FILE: PearcleanerHelper/CodesignCheck.swift ================================================ // // CodesignCheck.swift // // Created by Erik Berglund on 2018-10-01. // Copyright © 2018 Erik Berglund. All rights reserved. // import Foundation import Security let kSecCSDefaultFlags = 0 enum CodesignCheckError: Error { case message(String) } struct CodesignCheck { // MARK: - Compare Functions public static func codeSigningMatches(pid: pid_t) throws -> Bool { return try self.codeSigningCertificatesForSelf() == self.codeSigningCertificates(forPID: pid) } // MARK: - Public Functions public static func codeSigningCertificatesForSelf() throws -> [SecCertificate] { guard let secStaticCode = try secStaticCodeSelf() else { return [] } return try codeSigningCertificates(forStaticCode: secStaticCode) } public static func codeSigningCertificates(forPID pid: pid_t) throws -> [SecCertificate] { guard let secStaticCode = try secStaticCode(forPID: pid) else { return [] } return try codeSigningCertificates(forStaticCode: secStaticCode) } public static func codeSigningCertificates(forURL url: URL) throws -> [SecCertificate] { guard let secStaticCode = try secStaticCode(forURL: url) else { return [] } return try codeSigningCertificates(forStaticCode: secStaticCode) } // MARK: - Private Functions private static func executeSecFunction(_ secFunction: () -> (OSStatus) ) throws { let osStatus = secFunction() guard osStatus == errSecSuccess else { throw CodesignCheckError.message(String(describing: SecCopyErrorMessageString(osStatus, nil))) } } private static func secStaticCodeSelf() throws -> SecStaticCode? { var secCodeSelf: SecCode? try executeSecFunction { SecCodeCopySelf(SecCSFlags(rawValue: 0), &secCodeSelf) } guard let secCode = secCodeSelf else { throw CodesignCheckError.message("SecCode returned empty from SecCodeCopySelf") } return try secStaticCode(forSecCode: secCode) } private static func secStaticCode(forPID pid: pid_t) throws -> SecStaticCode? { var secCodePID: SecCode? try executeSecFunction { SecCodeCopyGuestWithAttributes(nil, [kSecGuestAttributePid: pid] as CFDictionary, [], &secCodePID) } guard let secCode = secCodePID else { throw CodesignCheckError.message("SecCode returned empty from SecCodeCopyGuestWithAttributes") } return try secStaticCode(forSecCode: secCode) } private static func secStaticCode(forURL url: URL) throws -> SecStaticCode? { var secStaticCodePath: SecStaticCode? try executeSecFunction { SecStaticCodeCreateWithPath(url as CFURL, [], &secStaticCodePath) } guard let secStaticCode = secStaticCodePath else { throw CodesignCheckError.message("SecStaticCode returned empty from SecStaticCodeCreateWithPath") } return secStaticCode } private static func secStaticCode(forSecCode secCode: SecCode) throws -> SecStaticCode? { var secStaticCodeCopy: SecStaticCode? try executeSecFunction { SecCodeCopyStaticCode(secCode, [], &secStaticCodeCopy) } guard let secStaticCode = secStaticCodeCopy else { throw CodesignCheckError.message("SecStaticCode returned empty from SecCodeCopyStaticCode") } return secStaticCode } private static func isValid(secStaticCode: SecStaticCode) throws { try executeSecFunction { SecStaticCodeCheckValidity(secStaticCode, SecCSFlags(rawValue: kSecCSDoNotValidateResources | kSecCSCheckNestedCode), nil) } } private static func secCodeInfo(forStaticCode secStaticCode: SecStaticCode) throws -> [String: Any]? { try isValid(secStaticCode: secStaticCode) var secCodeInfoCFDict: CFDictionary? try executeSecFunction { SecCodeCopySigningInformation(secStaticCode, SecCSFlags(rawValue: kSecCSSigningInformation), &secCodeInfoCFDict) } guard let secCodeInfo = secCodeInfoCFDict as? [String: Any] else { throw CodesignCheckError.message("CFDictionary returned empty from SecCodeCopySigningInformation") } return secCodeInfo } private static func codeSigningCertificates(forStaticCode secStaticCode: SecStaticCode) throws -> [SecCertificate] { guard let secCodeInfo = try secCodeInfo(forStaticCode: secStaticCode), let secCertificates = secCodeInfo[kSecCodeInfoCertificates as String] as? [SecCertificate] else { return [] } return secCertificates } } ================================================ FILE: PearcleanerHelper/com.alienator88.Pearcleaner.PearcleanerHelper.plist ================================================ Label com.alienator88.Pearcleaner.PearcleanerHelper BundleProgram Contents/MacOS/PearcleanerHelper MachServices com.alienator88.Pearcleaner.PearcleanerHelper AssociatedBundleIdentifiers com.alienator88.Pearcleaner ================================================ FILE: PearcleanerHelper/main.swift ================================================ // // main.swift // PearcleanerHelper // // Created by Alin Lupascu on 3/14/25. // import Foundation import ObjectiveC @objc(HelperToolProtocol) public protocol HelperToolProtocol { func runCommand(command: String, withReply reply: @escaping (Bool, String) -> Void) func runThinning(atPath: String, withReply reply: @escaping (Bool, String) -> Void) func runBundleThinning(bundlePath: String, withReply reply: @escaping (Bool, String, [String: UInt64]) -> Void) } // XPC Communication setup class HelperToolDelegate: NSObject, NSXPCListenerDelegate, HelperToolProtocol { private var activeConnections = Set() override init() { super.init() } // Accept new XPC connections by setting up the exported interface and object. func listener(_ listener: NSXPCListener, shouldAcceptNewConnection newConnection: NSXPCConnection) -> Bool { guard isValidClient(connection: newConnection) else { print("❌ Rejected connection from unauthorized client") return false } newConnection.exportedInterface = NSXPCInterface(with: HelperToolProtocol.self) newConnection.exportedObject = self newConnection.invalidationHandler = { [weak self] in self?.activeConnections.remove(newConnection) if self?.activeConnections.isEmpty == true { exit(0) // Exit when no active connections remain } } activeConnections.insert(newConnection) newConnection.resume() return true } // Execute the shell command and reply with output. func runCommand(command: String, withReply reply: @escaping (Bool, String) -> Void) { let process = Process() process.executableURL = URL(fileURLWithPath: "/bin/bash") process.arguments = ["-c", command] let pipe = Pipe() process.standardOutput = pipe process.standardError = pipe do { try process.run() process.waitUntilExit() } catch { reply(false, "Failed to run command: \(error.localizedDescription)") return } let data = pipe.fileHandleForReading.readDataToEndOfFile() let output = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" let success = (process.terminationStatus == 0) // Check if process exited successfully reply(success, output.isEmpty ? "No output" : output) } // Execute app lipo using privileges for apps owned by root func runThinning(atPath: String, withReply reply: @escaping (Bool, String) -> Void) { let success = thinBinaryUsingMachO(executablePath: atPath) reply(success, success ? "Success" : "Failed") } func runBundleThinning(bundlePath: String, withReply reply: @escaping (Bool, String, [String: UInt64]) -> Void) { let bundleURL = URL(fileURLWithPath: bundlePath) let result = thinAppBundle(at: bundleURL) let success = result.0 let message = success ? "Bundle thinning completed successfully" : "Bundle thinning failed" let sizes = result.1 ?? [:] reply(success, message, sizes) } // Check that the codesigning matches between the main app and the helper app private func isValidClient(connection: NSXPCConnection) -> Bool { do { return try CodesignCheck.codeSigningMatches(pid: connection.processIdentifier) } catch { print("Helper code signing check failed with error: \(error)") return false } } } // Set up and start the XPC listener. let delegate = HelperToolDelegate() let listener = NSXPCListener(machServiceName: "com.alienator88.Pearcleaner.PearcleanerHelper") listener.delegate = delegate listener.resume() RunLoop.main.run() ================================================ FILE: PearcleanerSentinel/FileWatcher.swift ================================================ // // FileWatcher.swift // Pearcleaner // // Created by Alin Lupascu on 9/2/25. // import Cocoa import Foundation public class FileWatcher { let filePaths: [String] // -- paths to watch - works on folders and file paths public var callback: (CallBack)? public var queue: DispatchQueue? var streamRef: FSEventStreamRef? var hasStarted: Bool { streamRef != nil } public init(_ paths: [String]) { self.filePaths = paths } /** * - Parameters: * - streamRef: The stream for which event(s) occurred. clientCallBackInfo: The info field that was supplied in the context when this stream was created. * - numEvents: The number of events being reported in this callback. Each of the arrays (eventPaths, eventFlags, eventIds) will have this many elements. * - eventPaths: An array of paths to the directories in which event(s) occurred. The type of this parameter depends on the flags * - eventFlags: An array of flag words corresponding to the paths in the eventPaths parameter. If no flags are set, then there was some change in the directory at the specific path supplied in this event. See FSEventStreamEventFlags. * - eventIds: An array of FSEventStreamEventIds corresponding to the paths in the eventPaths parameter. Each event ID comes from the most recent event being reported in the corresponding directory named in the eventPaths parameter. */ let eventCallback: FSEventStreamCallback = {( stream: ConstFSEventStreamRef, contextInfo: UnsafeMutableRawPointer?, numEvents: Int, eventPaths: UnsafeMutableRawPointer, eventFlags: UnsafePointer, eventIds: UnsafePointer ) in let fileSystemWatcher = Unmanaged.fromOpaque(contextInfo!).takeUnretainedValue() let paths = Unmanaged.fromOpaque(eventPaths).takeUnretainedValue() as! [String] (0...fromOpaque(info!).retain() return info } let releaseCallback: CFAllocatorReleaseCallBack = {(info: UnsafeRawPointer?) in Unmanaged.fromOpaque(info!).release() } func selectStreamScheduler() { let targetQueue = queue ?? DispatchQueue.main FSEventStreamSetDispatchQueue(streamRef!, targetQueue) } } /** * Convenient */ extension FileWatcher { convenience init( _ paths: [String], _ callback: @escaping (CallBack), _ queue: DispatchQueue ) { self.init(paths) self.callback = callback self.queue = queue } } /** * Actions */ extension FileWatcher { /** * Start listening for FSEvents */ public func start() { guard !hasStarted else { return } // -- make sure we are not already listening! var context = FSEventStreamContext( version: 0, info: Unmanaged.passUnretained(self).toOpaque(), retain: retainCallback, release: releaseCallback, copyDescription: nil ) streamRef = FSEventStreamCreate( kCFAllocatorDefault, eventCallback, &context, filePaths as CFArray, FSEventStreamEventId(kFSEventStreamEventIdSinceNow), 0, UInt32(kFSEventStreamCreateFlagUseCFTypes | kFSEventStreamCreateFlagFileEvents) ) selectStreamScheduler() FSEventStreamStart(streamRef!) } /** * Stop listening for FSEvents */ public func stop() { guard hasStarted else { return } // -- make sure we are indeed listening! FSEventStreamStop(streamRef!) FSEventStreamInvalidate(streamRef!) FSEventStreamRelease(streamRef!) streamRef = nil } } /** * Callback signature */ extension FileWatcher { public typealias CallBack = (_ fileWatcherEvent: FileWatcherEvent) -> Void } /** * - Parameters: * - id: is an id number that the os uses to differentiate between events. * - path: is the path the change took place. its formated like so: Users/John/Desktop/test/text.txt * - flag: pertains to the file event type. * ## Examples: * let url = NSURL(fileURLWithPath: event.path)//<--formats paths to: file:///Users/John/Desktop/test/text.txt * Swift.print("fileWatcherEvent.fileChange: " + "\(event.fileChange)") * Swift.print("fileWatcherEvent.fileModified: " + "\(event.fileModified)") * Swift.print("\t eventId: \(event.id) - eventFlags: \(event.flags) - eventPath: \(event.path)") */ public class FileWatcherEvent { public var id: FSEventStreamEventId public var path: String public var flags: FSEventStreamEventFlags init(_ eventId: FSEventStreamEventId, _ eventPath: String, _ eventFlags: FSEventStreamEventFlags) { self.id = eventId self.path = eventPath self.flags = eventFlags } } /** * The following code is to differentiate between the FSEvent flag types (aka file event types) * - Remark: Be aware that .DS_STORE changes frequently when other files change */ extension FileWatcherEvent { // General var fileChange: Bool { (flags & FSEventStreamEventFlags(kFSEventStreamEventFlagItemIsFile)) != 0 } var dirChange: Bool { (flags & FSEventStreamEventFlags(kFSEventStreamEventFlagItemIsDir)) != 0 } // CRUD var created: Bool { (flags & FSEventStreamEventFlags(kFSEventStreamEventFlagItemCreated)) != 0 } var removed: Bool { (flags & FSEventStreamEventFlags(kFSEventStreamEventFlagItemRemoved)) != 0 } var renamed: Bool { (flags & FSEventStreamEventFlags(kFSEventStreamEventFlagItemRenamed)) != 0 } var modified: Bool { (flags & FSEventStreamEventFlags(kFSEventStreamEventFlagItemModified)) != 0 } } /** * Convenince */ extension FileWatcherEvent { // File public var fileCreated: Bool { fileChange && created } public var fileRemoved: Bool { fileChange && removed } public var fileRenamed: Bool { fileChange && renamed } public var fileModified: Bool { fileChange && modified } // Directory public var dirCreated: Bool { dirChange && created } public var dirRemoved: Bool { dirChange && removed } public var dirRenamed: Bool { dirChange && renamed } public var dirModified: Bool { dirChange && modified } } /** * Simplifies debugging * ## Examples: * Swift.print(event.description) // Outputs: The file /Users/John/Desktop/test/text.txt was modified */ extension FileWatcherEvent { public var description: String { var result = "The \(fileChange ? "file":"directory") \(self.path) was" if self.removed { result += " removed" } else if self.created { result += " created" } else if self.renamed { result += " renamed" } else if self.modified { result += " modified" } return result } } ================================================ FILE: PearcleanerSentinel/com.alienator88.PearcleanerSentinel.plist ================================================ Label com.alienator88.PearcleanerSentinel BundleProgram Contents/MacOS/PearcleanerSentinel RunAtLoad KeepAlive AssociatedBundleIdentifiers com.alienator88.Pearcleaner ================================================ FILE: PearcleanerSentinel/main.swift ================================================ // // main.swift // PearcleanerSentinel // // Created by Alin Lupascu on 11/9/23. // import AppKit main() var globalFileWatcher: FileWatcher? func startGlobalFileWatcher() { let home = FileManager.default.homeDirectoryForCurrentUser.path globalFileWatcher = FileWatcher(["\(home)/.Trash"]) globalFileWatcher?.queue = DispatchQueue.global() globalFileWatcher?.callback = { event in checkApp(file: event.path) } globalFileWatcher?.start() } func stopGlobalFileWatcher() { globalFileWatcher?.stop() globalFileWatcher = nil } func setupNotificationListener() { let notificationCenter = DistributedNotificationCenter.default() notificationCenter.addObserver(forName: Notification.Name("Pearcleaner.StartFileWatcher"), object: nil, queue: nil) { notification in print("Received start notification") startGlobalFileWatcher() } notificationCenter.addObserver(forName: Notification.Name("Pearcleaner.StopFileWatcher"), object: nil, queue: nil) { notification in print("Received stop notification") stopGlobalFileWatcher() } } func main() { setupNotificationListener() startGlobalFileWatcher() RunLoop.main.run() } func checkApp(file: String) { let app = URL(fileURLWithPath: file) let appExt = app.pathExtension if appExt == "app" { if let appBundle = Bundle(url: app) { if appBundle.bundleIdentifier == "com.alienator88.Pearcleaner" { return } else { if FileManager.default.isInTrash(app) { NSWorkspace.shared.open(URL(string: "pear://openApp?path=\(file)")!) } } } else { print("Error: Unable to get bundle information for \(file)") } } } // --- Trash Relationship --- extension FileManager { public func isInTrash(_ file: URL) -> Bool { var relationship: URLRelationship = .other do { try getRelationship(&relationship, of: .trashDirectory, in: .userDomainMask, toItemAt: file) return relationship == .contains } catch { return false } } } ================================================ FILE: README.md ================================================ # Pearcleaner ### Project Status: > As you may have noticed, there haven't been recent updates made to the app and I wanted to provide some details. > I have a full time job on the side, but most recently I took a break for the holidays and right after that I joined a friend who is starting a SaaS company. For now, I just legitimately don't have any spare time to work on my open-source apps. Once things slow down in the future and I have more free time, I'll get back to tackling the submitted issues/requests. If anybody has Swift/SwiftUI experience and wants to work on the existing issues meanwhile, feel free to submit PRs and I'll review/approve as I can. Thank you!


            Status: Maintained
            Version: 5.4.3
            Download · Commits


            A free, source-available and fair-code licensed Mac app cleaner inspired by [Freemacsoft's AppCleaner](https://freemacsoft.net/appcleaner/) and [Sun Knudsen's Privacy Guides](https://github.com/sunknudsen/guides/tree/main/archive/how-to-clean-uninstall-macos-apps-using-appcleaner-open-source-alternative) post on his app-cleaner script. This project was born out of wanting to learn more on how macOS deals with app installation/uninstallation and getting more Swift experience. If you have suggestions I'm open to hearing them, submit a feature request! ### Table of Contents: [Features](#features) | [Screenshots](#screenshots) | [Issues](#issues) | [Requirements](#requirements) | [Download](#getting-pearcleaner) | [Translations](#translations) | [License](#license) | [Thanks](#thanks) | [Other Apps](#other-apps)
            ## Features ### Core - **App Uninstall • Orphaned File Search • Development Environment Manager • File Search • Homebrew Manager • App Lipo • PKG Manager • Plugin Manager • Services Manager • Apps Updater** - Drag/drop apps, CLI support, and deep link automation [view](https://github.com/alienator88/Pearcleaner/wiki/Deep-Link-Guide) - List or Grid view with badges for web/iOS apps - Finder Extension for right-click uninstall - Pearcleaner self-uninstall and other options ### Utilities - Prune unused app translations, keeping only preferred languages - Strip unneeded architectures from universal apps without requirement of lipo binary from xcode tools - **Sentinel Monitor**: Automatic cleanup when apps hit Trash (~2MB RAM) - Export app bundles and file lists - Basic Steam games support ### Customization - Theme system with custom colors - Include/exclude directories for searching - Adjustable search sensitivity ## Screenshots

            ## Issues > [!WARNING] > - When submitting issues, please use the appropriate issue template corresponding with your problem [HERE](https://github.com/alienator88/Pearcleaner/issues/new/choose) > - Issues with no template will be closed > - This is a personal/hobby app, therefore the project is fairly opinionated. Opinion-based requests (e.g., “the layout would look better this way”) will not be considered. ## Requirements > [!NOTE] > - Full Disk permission to search for files > - Privileged Helper to perform actions on system folders | macOS Version | Codename | Supported | |---------------|----------|-----------| | 13.x | Ventura | ✅ | | 14.x | Sonoma | ✅ | | 15.x | Sequoia | ✅ | | 26.x | Tahoe | ✅ | | TBD | Beta | ❌ | > Versions prior to macOS 13.0 are not supported due to missing Swift/SwiftUI APIs required by the app. ## Getting Pearcleaner
            Releases Pre-compiled, always up-to-date versions are available from my [releases](https://github.com/alienator88/Pearcleaner/releases) page.
            Homebrew You can add the app via Homebrew: ``` brew install --cask pearcleaner ```
            ## Translations If you are able to contribute to translations for the app, please see this discussion: https://github.com/alienator88/Pearcleaner/discussions/137 ## License > [!IMPORTANT] > Pearcleaner is licensed under Apache 2.0 with [Commons Clause](https://commonsclause.com/). This means that you can do anything you'd like with the source, modify it, contribute to it, etc., but the license explicitly prohibits any form of monetization for Pearcleaner or any modified versions of it. See full license [HERE](https://github.com/alienator88/Pearcleaner/blob/main/LICENSE.md) ## Thanks - Much appreciation to [Freemacsoft's AppCleaner](https://freemacsoft.net/appcleaner/) and [Sun Knudsen's app-cleaner script](https://github.com/sunknudsen/guides/tree/main/archive/how-to-clean-uninstall-macos-apps-using-appcleaner-open-source-alternative) for the inspiration - [DharsanB](https://github.com/dharsanb) for sponsoring my Apple Developer account ## Some of my apps [Pearcleaner](https://github.com/alienator88/Pearcleaner) - An opensource app cleaner with privacy in mind [Sentinel](https://github.com/alienator88/Sentinel) - A GUI for controlling gatekeeper status on your Mac [Viz](https://github.com/alienator88/Viz) - Utility for extracting text from images, videos, qr/barcodes [PearHID](https://github.com/alienator88/PearHID) - Remap your macOS keyboard with a simple SwiftUI frontend ================================================ FILE: Shared/AppGroupDefaults.swift ================================================ // // AppGroupDefaults.swift // Pearcleaner // // Created by Alin Lupascu on 9/30/25. // import Foundation extension UserDefaults { static let appGroup = UserDefaults(suiteName: "group.com.alienator88.Pearcleaner")! struct Keys { static let showAppIconInMenu = "showAppIconInMenu" } // Setting for showing app icon in context menu static var showAppIconInMenu: Bool { get { return appGroup.bool(forKey: Keys.showAppIconInMenu) } set { appGroup.set(newValue, forKey: Keys.showAppIconInMenu) } } } ================================================ FILE: announcements.json ================================================ { "4.4.0": { "features": [ "Privileged Helper Service: Pearcleaner now supports using a privileged helper service to perform operations that require administrative permissions. This will eventually replace the current mechanism which uses Authorization Services as it's deprecated by Apple. More information in Settings > Helper tab.", "App Lipo: There's a new Lipo(beta) view available in the menu which will show all universal apps and strip the app binary of the unused architectures to save some space. Universal apps will also show a Lipo button on the App Details view next to the Uninstall button to perform the same procedure on each app individually." ], "caveats": [] }, "3.9.2": { "features": [ "Multilingual Support: If you would like to help translate Pearcleaner to your language, please see GitHub issue #83" ], "caveats": [] }, "3.7.0": { "features": [ "App List Size Sorting: You can now sort the sidebar app list alphabetically or by descending size. Click the User/System header to toggle or from the searchbar menu", "Leftover Files: On first use, a warning/explanation sheet will be presented to the user" ], "caveats": [] }, "3.5.0": { "features": [ "Finder Extension: When enabled, you can right click an app in finder and uninstall with Pearcleaner directly", "Theme System: You can now select a base theme color for the application window" ], "caveats": [] }, "3.3.0": { "features": [ "Folder Settings: Add more directories where Pearcleaner should search for app files", "Progress: Show progress bar on startup when loading all app files with instant search enabled", "App Icon: Show background color behind app icons in FilesView based on icon's average color mapping", "Progress: Show time counter on regular file search views" ], "caveats": [] }, "3.2.0": { "features": [ "Menubar Item: You can choose to access Pearcleaner from the menubar. This will show a slightly modified Mini Mode view." ], "caveats": [] }, "3.1.0": { "features": [ "Homebrew Cleanup: If you install apps using brew, you can now enable Homebrew cleanup in the settings to have Pearcleaner alert Homebrew that the app was removed externally from Homebrew and keep the brew app list synced." ], "caveats": [] }, "3.0.0": { "features": [ "Instant Search: Enable in settings to load app files on startup", "Semantic Versioning: Going forward will use semver (Ex. v0.0.0)", "Feature Alert: For each version, a feature alert will popup once on startup to show details", "Leftover File Cleaning: Search your Mac for files leftover by uninstalled apps", "Sidebar Drag Handle: Resize the sidebar in regular mode", "Redesign UI/Settings/Icon: Some new buttons, layout changes, new official app icon and pear color theme accents", "File Sort: Sort files alphabetically or by size", "Socket File Removal: Finds socket files that aren't even visible in Finder with show hidden files enabled", "OSLog Output: Will print errors to the Console app for easier troubleshooting" ], "caveats": [] } }