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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions PureMac.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@
3A8D39C3C250F26302EC45AB /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
3BBCB7D8C9A325A7FFEEF391 /* ProcessResourceMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessResourceMonitorTests.swift; sourceTree = "<group>"; };
3D4FD34378988D430A582ED0 /* OnboardingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingView.swift; sourceTree = "<group>"; };
409E15E7364CABDF1435C153 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
40A3F22FCEF534DE4A2CAD0E /* Haptics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Haptics.swift; sourceTree = "<group>"; };
40CD4855FB5FA866CAA52C10 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = "<group>"; };
4144A1B8F1D52D44A6E8E726 /* SimilarPhotoFinderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimilarPhotoFinderTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -224,6 +225,7 @@
F270D47D1DD94D4577482B90 /* ProtectionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProtectionView.swift; sourceTree = "<group>"; };
F31F91226CDCFBB8E303B7DA /* AppConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppConstants.swift; sourceTree = "<group>"; };
F3C3342D6386FBB6DE3D18BC /* SimilarPhotosView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SimilarPhotosView.swift; sourceTree = "<group>"; };
F42864F392371F26932F20B8 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/ServicesMenu.strings"; sourceTree = "<group>"; };
F661A0F64CF93E482CB1728F /* DashboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardView.swift; sourceTree = "<group>"; };
F77B9D7D5353CD42BD03ADBB /* ApplicationUpdaterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationUpdaterTests.swift; sourceTree = "<group>"; };
F9DCBD97CC88D77AE8DB01A4 /* PermissionCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionCoordinator.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -713,6 +715,7 @@
6ED5C9C10C69C3143B1F5CA6 /* ru */,
374A3BE46E71E54B6927E9EF /* uk */,
1B8BE07AD70610A91E735E78 /* zh-Hans */,
F42864F392371F26932F20B8 /* zh-Hant */,
);
name = ServicesMenu.strings;
sourceTree = "<group>";
Expand All @@ -727,6 +730,7 @@
ED0C4D8912DA94F81B2AEF8F /* ru */,
DFC4A1FD7DB92C82725B4314 /* uk */,
AD97E55BA95BE2AEA858A80B /* zh-Hans */,
409E15E7364CABDF1435C153 /* zh-Hant */,
);
name = InfoPlist.strings;
sourceTree = "<group>";
Expand Down
8 changes: 8 additions & 0 deletions PureMac/zh-Hant.lproj/InfoPlist.strings
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/* macOS 權限提示與版權資訊(台灣繁體中文) */
"NSAppleEventsUsageDescription" = "PureMac 會使用 Apple Events 與 Finder 互動,以顯示檔案位置。";
"NSDesktopFolderUsageDescription" = "PureMac 需要存取「桌面」,以尋找垃圾檔案與殘留檔案。";
"NSDocumentsFolderUsageDescription" = "PureMac 需要存取「文件」,以尋找垃圾檔案與殘留檔案。";
"NSDownloadsFolderUsageDescription" = "PureMac 需要存取「下載項目」,以尋找垃圾檔案與殘留檔案。";
"NSRemovableVolumesUsageDescription" = "PureMac 需要存取卸除式儲存裝置,以掃描垃圾檔案。";
"NSSystemAdministrationUsageDescription" = "PureMac 需要管理者權限,才能清理快取並完整解除安裝應用程式。";
"NSHumanReadableCopyright" = "版權所有 © 2026。採用 MIT 授權條款。";
1,282 changes: 641 additions & 641 deletions PureMac/zh-Hant.lproj/Localizable.strings

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions PureMac/zh-Hant.lproj/ServicesMenu.strings
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/* Finder 服務選單(台灣繁體中文) */
"Uninstall with PureMac" = "使用 PureMac 解除安裝";
35 changes: 34 additions & 1 deletion PureMacTests/LocalizationFilesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ final class LocalizationFilesTests: XCTestCase {
}

func testBuiltAppBundleContainsAddedLocalizations() throws {
for language in ["de", "it", "ru", "uk", "zh-Hans"] {
for language in ["de", "it", "ru", "uk", "zh-Hans", "zh-Hant"] {
XCTAssertTrue(
Bundle.main.localizations.contains(language),
"Expected the built app bundle to register the \(language) localization"
Expand Down Expand Up @@ -106,6 +106,39 @@ final class LocalizationFilesTests: XCTestCase {
}
}

// 台灣繁中應完整翻譯介面文字,產品名稱則保留原文。
func testTraditionalChineseHasNoUntranslatedInterfaceText() throws {
let files = try localizableStringsFiles()
let english = try localizedStrings(in: XCTUnwrap(files["en"]))
let traditionalChinese = try localizedStrings(in: XCTUnwrap(files["zh-Hant"]))
let retainedNames: Set<String> = [
"PureMac", "PureMac.app", "CPU", "Finder", "FileVault",
"Gatekeeper", "Homebrew", "Time Machine", "XProtect"
]
let untranslated = english.keys.filter {
!retainedNames.contains($0) && traditionalChinese[$0] == english[$0]
}.sorted()
XCTAssertTrue(untranslated.isEmpty, "繁中介面仍有未翻譯文字:\(untranslated)")
}

// 權限提示和 Finder 服務選單也必須有完整的繁中資源。
func testTraditionalChineseAuxiliaryResourcesHaveEnglishKeyParity() throws {
let sourceRoot = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent().deletingLastPathComponent()
.appendingPathComponent("PureMac")
for resource in ["InfoPlist", "ServicesMenu"] {
let english = try localizedKeys(in: sourceRoot
.appendingPathComponent("en.lproj/\(resource).strings"))
let traditionalChinese = try localizedStrings(in: sourceRoot
.appendingPathComponent("zh-Hant.lproj/\(resource).strings"))
XCTAssertEqual(english, Set(traditionalChinese.keys))
for value in traditionalChinese.values {
XCTAssertNotNil(value.range(of: "\\p{Han}", options: .regularExpression),
"繁中輔助資源必須包含中文翻譯:\(value)")
}
}
}

private func localizableStringsFiles() throws -> [String: URL] {
let sourceRoot = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent()
Expand Down
8 changes: 6 additions & 2 deletions PureMacTests/PerformanceInspectorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ final class PerformanceInspectorTests: XCTestCase {
XCTAssertEqual(item.arguments, ["--background"])
XCTAssertEqual(item.domain, .userAgent)
XCTAssertTrue(item.isDisabled)
XCTAssertEqual(item.triggers, ["At login", "Conditional keep alive", "Every 300 seconds"])
XCTAssertEqual(item.triggers, [
String(localized: "At login"),
String(localized: "Conditional keep alive"),
String(format: String(localized: "Every %lld seconds"), Int64(300))
])
XCTAssertEqual(item.sourceURL, url)
}

Expand All @@ -60,7 +64,7 @@ final class PerformanceInspectorTests: XCTestCase {
XCTAssertEqual(item.label, "com.example.helper")
XCTAssertEqual(item.program, "/usr/local/bin/example-helper")
XCTAssertEqual(item.arguments, ["--mode", "quiet"])
XCTAssertEqual(item.triggers, ["Watches paths"])
XCTAssertEqual(item.triggers, [String(localized: "Watches paths")])
XCTAssertFalse(item.isDisabled)
}

Expand Down
6 changes: 3 additions & 3 deletions PureMacTests/ProtectionAuditTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ final class ProtectionAuditTests: XCTestCase {
)

XCTAssertEqual(finding.status, .unknown)
XCTAssertEqual(finding.detail, "The system returned an unrecognized status.")
XCTAssertEqual(finding.detail, String(localized: "The system returned an unrecognized status."))
}

func testCommandFailureCannotBeReportedAsEnabled() {
Expand All @@ -71,7 +71,7 @@ final class ProtectionAuditTests: XCTestCase {
)

XCTAssertEqual(finding.status, .unknown)
XCTAssertEqual(finding.detail, "PureMac could not read this setting.")
XCTAssertEqual(finding.detail, String(localized: "PureMac could not read this setting."))
}

func testTimeoutIsUnknown() {
Expand All @@ -88,7 +88,7 @@ final class ProtectionAuditTests: XCTestCase {
)

XCTAssertEqual(finding.status, .unknown)
XCTAssertEqual(finding.detail, "The check timed out.")
XCTAssertEqual(finding.detail, String(localized: "The check timed out."))
}

func testAuditPreservesCheckOrder() async {
Expand Down
16 changes: 11 additions & 5 deletions docs/README.zh-Hant.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

<p align="center">
<b>免費、開源的 macOS 應用程式管理與系統清理工具。</b><br>
徹底解除安裝應用程式。尋找孤立檔案。清理系統垃圾。<br>
徹底解除安裝應用程式。尋找殘留檔案。清理系統垃圾。<br>
無訂閱。無遙測。無資料收集。
</p>

Expand Down Expand Up @@ -65,6 +65,12 @@ xcodebuild -project PureMac.xcodeproj -scheme PureMac -configuration Release -de
open build/Build/Products/Release/PureMac.app
```

## 介面語言

開啟 PureMac 的「設定 → 一般 → 語言」,選擇「繁體中文」,再按一下「立即重新啟動」套用。

繁體中文介面採用台灣慣用詞彙,涵蓋主畫面、工具頁、設定、權限提示與 Finder 服務選單。PureMac、Finder、Homebrew 等產品名稱保留原文。

## 功能

### 應用程式解除安裝
Expand All @@ -75,16 +81,16 @@ open build/Build/Products/Release/PureMac.app
- 系統應用程式保護 — 排除 27 個 Apple 應用程式,避免誤刪
- 主從檢視:左側為應用程式列表,右側為發現的檔案

### 孤立檔案搜尋
### 殘留檔案搜尋
- 偵測 `~/Library` 中已解除安裝應用程式留下的殘餘檔案
- 將 Library 內容與所有已安裝應用程式的識別碼比對
- 一鍵清除孤立檔案
- 一鍵清除殘留檔案

### 系統清理
- **智慧掃描** — 一鍵掃描所有分類
- **系統垃圾** — 系統快取、記錄與暫存檔案
- **使用者快取** — 動態發現所有應用程式快取(不需寫死清單)
- **AI 應用程式** — Ollama 與 LM Studio 日誌、快取,以及可選的本機歷史記錄清理
- **AI 應用程式** — Ollama 與 LM Studio 記錄檔、快取,以及可選的本機歷史記錄清理
- **郵件附件** — 已下載的郵件附件
- **垃圾桶** — 清空所有垃圾桶
- **大型與舊檔案** — 超過 100 MB 或超過 1 年的檔案
Expand Down Expand Up @@ -134,7 +140,7 @@ PureMac/
Views/ - 原生 SwiftUI 視圖
Apps/ - 應用程式解除安裝視圖
Cleaning/ - 智慧掃描與分類視圖
Orphans/ - 孤立檔案搜尋
Orphans/ - 殘留檔案搜尋
Settings/ - 以原生 Form 為基礎的設定
Components/ - 共用元件
```
Expand Down