From 7a22c520a230cf4e63867001c0bd70b8f2e2370d Mon Sep 17 00:00:00 2001 From: Prime Build <162145141+PrimeBuild-pc@users.noreply.github.com> Date: Sat, 23 May 2026 16:13:17 +0200 Subject: [PATCH 1/7] Add latest release downloads badge --- README.md | 325 +++++++++++++++++++++++++++--------------------------- 1 file changed, 163 insertions(+), 162 deletions(-) diff --git a/README.md b/README.md index 3fc121f..3fdb304 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,34 @@ -
- -# ThreadPilot ✈️ - +
+ +# ThreadPilot ✈️ + **A free and open-source Windows process control center for deterministic CPU, priority, memory, and power-plan workflows.** - + [![Build](https://github.com/PrimeBuild-pc/ThreadPilot/actions/workflows/ci-devsecops.yml/badge.svg)](https://github.com/PrimeBuild-pc/ThreadPilot/actions/workflows/ci-devsecops.yml) [![codecov](https://codecov.io/gh/PrimeBuild-pc/ThreadPilot/branch/main/graph/badge.svg)](https://codecov.io/gh/PrimeBuild-pc/ThreadPilot) [![Release](https://img.shields.io/github/v/release/PrimeBuild-pc/ThreadPilot?sort=semver)](https://github.com/PrimeBuild-pc/ThreadPilot/releases) -[![winget](https://img.shields.io/winget/v/PrimeBuild.ThreadPilot?label=winget)](https://github.com/microsoft/winget-pkgs/tree/master/manifests/p/PrimeBuild/ThreadPilot) -[![Windows](https://img.shields.io/badge/Windows-11-blue?logo=windows)](https://www.microsoft.com/windows) -[![.NET](https://img.shields.io/badge/.NET-8.0-512BD4?logo=dotnet&logoColor=white)](https://dotnet.microsoft.com/) -[![License](https://img.shields.io/badge/License-AGPLv3-blue.svg)](LICENSE) -[![Issues](https://img.shields.io/github/issues/PrimeBuild-pc/ThreadPilot)](https://github.com/PrimeBuild-pc/ThreadPilot/issues) -[![Discussions](https://img.shields.io/badge/Discussions-GitHub-6f42c1?logo=github)](https://github.com/PrimeBuild-pc/ThreadPilot/discussions) - -[Install](#-install) • [Features](#-features) • [Screenshots](#-screenshots) • [Build](#-build-from-source) • [Support](#-support-the-project) - -
- -cover - -## What is ThreadPilot? - +[![Downloads](https://img.shields.io/github/downloads/PrimeBuild-pc/ThreadPilot/latest/total?label=latest%20downloads&logo=github)](https://github.com/PrimeBuild-pc/ThreadPilot/releases/latest) +[![winget](https://img.shields.io/winget/v/PrimeBuild.ThreadPilot?label=winget)](https://github.com/microsoft/winget-pkgs/tree/master/manifests/p/PrimeBuild/ThreadPilot) +[![Windows](https://img.shields.io/badge/Windows-11-blue?logo=windows)](https://www.microsoft.com/windows) +[![.NET](https://img.shields.io/badge/.NET-8.0-512BD4?logo=dotnet&logoColor=white)](https://dotnet.microsoft.com/) +[![License](https://img.shields.io/badge/License-AGPLv3-blue.svg)](LICENSE) +[![Issues](https://img.shields.io/github/issues/PrimeBuild-pc/ThreadPilot)](https://github.com/PrimeBuild-pc/ThreadPilot/issues) +[![Discussions](https://img.shields.io/badge/Discussions-GitHub-6f42c1?logo=github)](https://github.com/PrimeBuild-pc/ThreadPilot/discussions) + +[Install](#-install) • [Features](#-features) • [Screenshots](#-screenshots) • [Build](#-build-from-source) • [Support](#-support-the-project) + +
+ +cover + +## What is ThreadPilot? + ThreadPilot is a modern Windows process control center for users who want predictable control over process behavior, CPU affinity, CPU Sets, priority, memory priority, power plans, and saved process rules. It is designed as an open-source alternative for power users who need Process Lasso-style capabilities, automation support, system tray controls, and a Windows 11-first experience. ThreadPilot is not a performance overlay: its primary job is applying explicit process controls safely and making the result visible. - -## ✨ Features - + +## ✨ Features + - Live process management with refresh, filtering, context-menu actions, and a selected-process summary. - Topology-aware CPU affinity through `CpuSelection`, including CPU Sets support, processor groups, and safe handling for systems with more than 64 logical processors. - Safer CPU indexing in new affinity paths: CPU64 no longer aliases CPU0. @@ -43,57 +44,57 @@ It is designed as an open-source alternative for power users who need Process La - Administrator-aware Windows desktop workflow. - CI-backed build validation and package-manager distribution. - Windows 11 native visual refresh with neutral Fluent surfaces and refined card-based layouts. - -## 📦 Install - -### Install with WinGet - -ThreadPilot is available on WinGet as `PrimeBuild.ThreadPilot`. - -From **Command Prompt** or **PowerShell**: - -```cmd -winget install --id PrimeBuild.ThreadPilot -e -``` - -To refresh your local WinGet source first: - -```cmd -winget source update -winget search ThreadPilot -``` - -### Download from GitHub Releases - -[![Download Latest Release](https://img.shields.io/badge/Download-Latest%20Release-2ea44f?logo=github)](https://github.com/PrimeBuild-pc/ThreadPilot/releases/latest) -[![Portable ZIP Assets](https://img.shields.io/badge/Portable%20ZIP-Release%20Assets-1f6feb?logo=github)](https://github.com/PrimeBuild-pc/ThreadPilot/releases/latest) - -| Package | Recommended use | -|---|---| -| `ThreadPilot_v_Setup.exe` | Standard Windows installer for most users | -| `ThreadPilot_v_Portable.zip` | Portable/no-install deployment | - -Optional checksum verification: - -```powershell -Get-FileHash .\ThreadPilot_v_Setup.exe -Algorithm SHA256 -Get-FileHash .\ThreadPilot_v_Portable.zip -Algorithm SHA256 -``` - -Compare the result with `SHA256SUMS.txt` from the same release. - -## 🖼️ Screenshots - -banner - -## ⚙️ Requirements - -- Windows 11, build 22000 or newer. -- Administrator privileges to launch and manage system-level process settings. -- .NET 8 SDK only if you want to build from source. - -## 🚀 Usage Notes - + +## 📦 Install + +### Install with WinGet + +ThreadPilot is available on WinGet as `PrimeBuild.ThreadPilot`. + +From **Command Prompt** or **PowerShell**: + +```cmd +winget install --id PrimeBuild.ThreadPilot -e +``` + +To refresh your local WinGet source first: + +```cmd +winget source update +winget search ThreadPilot +``` + +### Download from GitHub Releases + +[![Download Latest Release](https://img.shields.io/badge/Download-Latest%20Release-2ea44f?logo=github)](https://github.com/PrimeBuild-pc/ThreadPilot/releases/latest) +[![Portable ZIP Assets](https://img.shields.io/badge/Portable%20ZIP-Release%20Assets-1f6feb?logo=github)](https://github.com/PrimeBuild-pc/ThreadPilot/releases/latest) + +| Package | Recommended use | +|---|---| +| `ThreadPilot_v_Setup.exe` | Standard Windows installer for most users | +| `ThreadPilot_v_Portable.zip` | Portable/no-install deployment | + +Optional checksum verification: + +```powershell +Get-FileHash .\ThreadPilot_v_Setup.exe -Algorithm SHA256 +Get-FileHash .\ThreadPilot_v_Portable.zip -Algorithm SHA256 +``` + +Compare the result with `SHA256SUMS.txt` from the same release. + +## 🖼️ Screenshots + +banner + +## ⚙️ Requirements + +- Windows 11, build 22000 or newer. +- Administrator privileges to launch and manage system-level process settings. +- .NET 8 SDK only if you want to build from source. + +## 🚀 Usage Notes + ThreadPilot uses an administrator-required manifest and requests elevation at startup. If UAC elevation is declined, the application exits instead of continuing in a limited mode. Persistent process rules are runtime-based. Apply at process start works only while ThreadPilot is running; it does not install a Windows Service, write registry or IFEO persistence, or use installer privilege tricks. @@ -101,91 +102,91 @@ Persistent process rules are runtime-based. Apply at process start works only wh ThreadPilot does not bypass anti-cheat or protected-process restrictions. Running as administrator may help with normal access-denied cases, but it does not override protected-process or anti-cheat enforcement. Useful startup arguments: - -```text ---start-minimized ---autostart ---test ---smoke-test -``` - -In `Power Plans > Custom Power Plans`, use `Add .pow File` to import custom power plans directly from the app. - -## 🧱 Build from Source - -```powershell -git clone https://github.com/PrimeBuild-pc/ThreadPilot.git -cd ThreadPilot -dotnet restore ThreadPilot_1.sln -dotnet build ThreadPilot_1.sln --configuration Release -dotnet run --project ThreadPilot.csproj --configuration Release -``` - -Run integrated runtime tests: - -```powershell -dotnet run --project ThreadPilot.csproj --configuration Release -- --test -``` - -Publish a self-contained Windows build: - -```powershell -dotnet publish ThreadPilot.csproj --configuration Release --runtime win-x64 --self-contained true -``` - -Build release artifacts with the project script: - -```powershell -./build/build-release.ps1 -``` - -## 🔐 Quality and Security - -- CI validates build, formatting, analyzers, vulnerability checks, and secret scanning. -- Security disclosures are handled through private GitHub advisories. See [`docs/SECURITY.md`](docs/SECURITY.md). -- Change history is tracked in [`docs/CHANGELOG.md`](docs/CHANGELOG.md). -- Coverage focuses on business/application code and excludes generated build artifacts. - -## 🧭 Project Documentation - -- [`docs/README.md`](docs/README.md) -- [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) -- [`docs/CODE_OF_CONDUCT.md`](docs/CODE_OF_CONDUCT.md) -- [`docs/RELEASE_SIGNING.md`](docs/RELEASE_SIGNING.md) -- [`docs/release/PACKAGING.md`](docs/release/PACKAGING.md) -- [`docs/reference/ARCHITECTURE_GUIDE.md`](docs/reference/ARCHITECTURE_GUIDE.md) -- [`docs/reference/DEVELOPER_GUIDE.md`](docs/reference/DEVELOPER_GUIDE.md) -- [`docs/reference/API_REFERENCE.md`](docs/reference/API_REFERENCE.md) -- [`docs/reference/PROJECT_STRUCTURE.md`](docs/reference/PROJECT_STRUCTURE.md) -- [`docs/reference/UI_STYLE_GUIDE.md`](docs/reference/UI_STYLE_GUIDE.md) - -## 🗺️ Roadmap - -- Expand unit and integration coverage for core services. -- Continue async reliability refactoring for long-running monitoring paths. -- Improve accessibility and localization readiness across major views. -- Formalize release signing and distribution hardening. - -## 🤝 Contributing - -Contributions are welcome. Before opening a pull request, please read [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) and [`docs/CODE_OF_CONDUCT.md`](docs/CODE_OF_CONDUCT.md). - -For bugs, feature requests, or packaging issues, open a GitHub issue with reproduction steps and your Windows version. - -## 📄 License - -ThreadPilot is licensed under the **GNU Affero General Public License v3.0**. See [`LICENSE`](LICENSE). - -## 💬 Support the Project - -
- -**Built with care for Windows power users.** - -[![GitHub Issues](https://img.shields.io/badge/Report%20a%20Bug-GitHub%20Issues-d73a49?logo=github)](https://github.com/PrimeBuild-pc/ThreadPilot/issues) -[![GitHub Discussions](https://img.shields.io/badge/Ask%20a%20Question-Discussions-6f42c1?logo=github)](https://github.com/PrimeBuild-pc/ThreadPilot/discussions) -[![PayPal](https://img.shields.io/badge/Support%20Development-PayPal-00457C?logo=paypal&logoColor=white)](https://paypal.me/PrimeBuildOfficial?country.x=IT&locale.x=it_IT) - -If ThreadPilot is useful to you, consider starring the repository, opening thoughtful issues, sharing feedback, or supporting development with a small donation. - -
+ +```text +--start-minimized +--autostart +--test +--smoke-test +``` + +In `Power Plans > Custom Power Plans`, use `Add .pow File` to import custom power plans directly from the app. + +## 🧱 Build from Source + +```powershell +git clone https://github.com/PrimeBuild-pc/ThreadPilot.git +cd ThreadPilot +dotnet restore ThreadPilot_1.sln +dotnet build ThreadPilot_1.sln --configuration Release +dotnet run --project ThreadPilot.csproj --configuration Release +``` + +Run integrated runtime tests: + +```powershell +dotnet run --project ThreadPilot.csproj --configuration Release -- --test +``` + +Publish a self-contained Windows build: + +```powershell +dotnet publish ThreadPilot.csproj --configuration Release --runtime win-x64 --self-contained true +``` + +Build release artifacts with the project script: + +```powershell +./build/build-release.ps1 +``` + +## 🔐 Quality and Security + +- CI validates build, formatting, analyzers, vulnerability checks, and secret scanning. +- Security disclosures are handled through private GitHub advisories. See [`docs/SECURITY.md`](docs/SECURITY.md). +- Change history is tracked in [`docs/CHANGELOG.md`](docs/CHANGELOG.md). +- Coverage focuses on business/application code and excludes generated build artifacts. + +## 🧭 Project Documentation + +- [`docs/README.md`](docs/README.md) +- [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) +- [`docs/CODE_OF_CONDUCT.md`](docs/CODE_OF_CONDUCT.md) +- [`docs/RELEASE_SIGNING.md`](docs/RELEASE_SIGNING.md) +- [`docs/release/PACKAGING.md`](docs/release/PACKAGING.md) +- [`docs/reference/ARCHITECTURE_GUIDE.md`](docs/reference/ARCHITECTURE_GUIDE.md) +- [`docs/reference/DEVELOPER_GUIDE.md`](docs/reference/DEVELOPER_GUIDE.md) +- [`docs/reference/API_REFERENCE.md`](docs/reference/API_REFERENCE.md) +- [`docs/reference/PROJECT_STRUCTURE.md`](docs/reference/PROJECT_STRUCTURE.md) +- [`docs/reference/UI_STYLE_GUIDE.md`](docs/reference/UI_STYLE_GUIDE.md) + +## 🗺️ Roadmap + +- Expand unit and integration coverage for core services. +- Continue async reliability refactoring for long-running monitoring paths. +- Improve accessibility and localization readiness across major views. +- Formalize release signing and distribution hardening. + +## 🤝 Contributing + +Contributions are welcome. Before opening a pull request, please read [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) and [`docs/CODE_OF_CONDUCT.md`](docs/CODE_OF_CONDUCT.md). + +For bugs, feature requests, or packaging issues, open a GitHub issue with reproduction steps and your Windows version. + +## 📄 License + +ThreadPilot is licensed under the **GNU Affero General Public License v3.0**. See [`LICENSE`](LICENSE). + +## 💬 Support the Project + +
+ +**Built with care for Windows power users.** + +[![GitHub Issues](https://img.shields.io/badge/Report%20a%20Bug-GitHub%20Issues-d73a49?logo=github)](https://github.com/PrimeBuild-pc/ThreadPilot/issues) +[![GitHub Discussions](https://img.shields.io/badge/Ask%20a%20Question-Discussions-6f42c1?logo=github)](https://github.com/PrimeBuild-pc/ThreadPilot/discussions) +[![PayPal](https://img.shields.io/badge/Support%20Development-PayPal-00457C?logo=paypal&logoColor=white)](https://paypal.me/PrimeBuildOfficial?country.x=IT&locale.x=it_IT) + +If ThreadPilot is useful to you, consider starring the repository, opening thoughtful issues, sharing feedback, or supporting development with a small donation. + +
\ No newline at end of file From 340610dfd3fd1552a637661c7efefd36f8668cc1 Mon Sep 17 00:00:00 2001 From: Prime Build <162145141+PrimeBuild-pc@users.noreply.github.com> Date: Sat, 6 Jun 2026 18:04:12 +0200 Subject: [PATCH 2/7] Add safe localization support Adds English/Chinese localization infrastructure with en-US as default, zh-CN as optional language, safe fallback behavior, settings integration, localized notifications, and expanded test coverage. --- App.xaml | 1 + App.xaml.cs | 2 + Locales/en-US.xaml | 529 ++++++++++++++++++ Locales/zh-CN.xaml | 529 ++++++++++++++++++ Models/ApplicationSettingsModel.cs | 4 + Services/ApplicationSettingsService.cs | 20 +- Services/ILocalizationService.cs | 44 ++ Services/LocalizationService.cs | 269 +++++++++ Services/NotificationService.cs | 93 ++- Services/ServiceConfiguration.cs | 1 + .../ApplicationSettingsModelTests.cs | 15 + .../ApplicationSettingsServiceTests.cs | 37 ++ .../LocalizationServiceTests.cs | 200 +++++++ .../NotificationServiceLocalizationTests.cs | 161 ++++++ .../SettingsViewModelThemeTests.cs | 71 ++- ViewModels/SettingsViewModel.cs | 38 ++ Views/SettingsView.xaml | 37 +- 17 files changed, 2018 insertions(+), 33 deletions(-) create mode 100644 Locales/en-US.xaml create mode 100644 Locales/zh-CN.xaml create mode 100644 Services/ILocalizationService.cs create mode 100644 Services/LocalizationService.cs create mode 100644 Tests/ThreadPilot.Core.Tests/LocalizationServiceTests.cs create mode 100644 Tests/ThreadPilot.Core.Tests/NotificationServiceLocalizationTests.cs diff --git a/App.xaml b/App.xaml index 592dae1..58f3c0d 100644 --- a/App.xaml +++ b/App.xaml @@ -10,6 +10,7 @@ + diff --git a/App.xaml.cs b/App.xaml.cs index 9aa438f..4916d2a 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -220,10 +220,12 @@ protected override void OnStartup(StartupEventArgs e) { var settingsService = this.ServiceProvider.GetRequiredService(); var themeService = this.ServiceProvider.GetRequiredService(); + var localizationService = this.ServiceProvider.GetRequiredService(); Task.Run(async () => await settingsService.LoadSettingsAsync()).GetAwaiter().GetResult(); var settings = settingsService.Settings; loadedSettings = settings; + localizationService.ApplyLanguage(settings.Language); effectiveStartMinimized = startMinimized || settings.StartMinimized; var useDarkTheme = settings.HasUserThemePreference ? settings.UseDarkTheme diff --git a/Locales/en-US.xaml b/Locales/en-US.xaml new file mode 100644 index 0000000..41967ab --- /dev/null +++ b/Locales/en-US.xaml @@ -0,0 +1,529 @@ + + + + ThreadPilot - Process & Power Plan Manager + Administrator Privileges Recommended + ThreadPilot is running with limited privileges. Some features may not be available. + Administrator access is required for: + • Modifying power plan configurations + • Changing process CPU affinity and priority + • Applying system-level optimizations and tweaks + • Managing protected processes + Continue Without Elevation + Request Elevation + + + Process Management + CPU Masks + Power Plans + Rules + Diagnostics + ThreadPilot Activity + Tweaks + Settings + + + Startup minimized + Enable Startup minimized when you want ThreadPilot to start silently in the tray on future Windows sign-ins. + Recommended + Don't show again + Open Settings + + + ThreadPilot Activity + Shows actions performed by ThreadPilot, including applied rules, affinity changes, priority changes, power plan changes, settings changes, tweaks, optimizations, and safe failure messages. + Search: + Category: + Level: + From: + To: + Refresh + Clear Display + Export Activity + Cleanup Diagnostic Files + Open Diagnostic Folder + + + Time + Status + Category + Message + Details + ID + Copy Entry + Refresh + No log entries to display + Adjust filters or refresh logs to load recent ThreadPilot activity. + + + Activity Details + Timestamp: + Status: + Category: + Message: + Details: + Correlation ID: + No log entry selected + Files: + Size: + Debug Diagnostics + Max Size (MB): + Retention (Days): + Save Settings + + + CPU Masks + Create reusable CPU affinity presets for per-process use. + Selecting a mask here only edits the preset. It does not change CPU affinity until you apply it to a process or save it in a process rule. + New + Delete + Duplicate Mask + Mask Information + Name: + Description: + Enabled + Default preset + Pre-selected when ThreadPilot needs a fallback mask or when creating per-process rules. It does not apply CPU affinity automatically. + Disable to temporarily hide this mask + Select CPUs + Toggle CPUs to edit this mask preset. Changes are saved automatically and do not affect running processes. All Cores is the protected default preset. + Mask names, descriptions, CPU selections, and options are saved automatically. + Create a new CPU mask + Delete selected mask + CPUs + CPU + + + Optional Diagnostics + Diagnostics are optional and intended for troubleshooting. For in-game overlays and detailed performance graphs, use dedicated tools. + Quick tips: + 1. Open diagnostics only when you need a focused troubleshooting snapshot. + 2. Review hotspots only as a hint before creating automation rules. + 3. Stop live metrics when you are done troubleshooting. + Continue to Diagnostics + + Active Processes + Top CPU and memory consumers with one-click rule creation. + Search processes by name + Rule-backed only + Actionable only + Rule Impact + Create/Update Rule from Selection + Create or update an automation rule from the selected hotspot process + + Stability Timeline + Clear History + Clear the displayed metrics and timeline history + Core Snapshot + Per-core utilization for quick imbalance checks. + Toggle Core Panel + Toggle Process Panel + Start Live Metrics + Start live metrics collection for this dashboard + Stop Live Metrics + Pause live metrics collection; background automation is separate + Refresh + Refresh the current dashboard snapshot + + Global Power Plan + Memory Used + CPU % + Mem % + Threads + Events + Severity + Detail + Time + Category + Process + Selected hotspot process + Metrics + Processes + + + Power Plans + Manage the active Windows power plan and import custom .pow profiles + Windows Power Plans + Select the Windows plan to make active. + Set as Active Windows Plan + Change the active Windows power plan to the selected plan + Refresh Plans + Refresh the list of available power plans + Custom Power Plans + Local .pow files ready to import into Windows. + Import Selected .pow + Import the selected custom .pow file into Windows + Add .pow File + Add a new custom power plan file to the custom library + Delete + Active + + + Rules & Automation + Automation monitoring watches process start/stop events and applies configured power plan, CPU mask, and priority rules. + Rule Editor + Create a rule from an executable or running process to automate power plan, CPU mask, and priority behavior. + Executable Match: + Match by Path + Match processes by full path instead of just executable name + Browse for Executable + Click to select an executable file (.exe) from your computer + Choose an executable. Match by path is more precise; matching by name applies to any process with that executable name. + Use Running Process: + Select a currently running process to use its executable + Use Selected Process + Use the executable from the selected running process + Rule Power Plan (global switch): + When this rule matches, Windows switches the global active power plan. + Rule CPU Mask: + Optional: Select a CPU mask to apply when this process starts + Rule Priority + Optional: Select the process priority to apply when this process starts + Association Priority: + Higher priority associations take precedence when multiple match + Add Rule + Update Selected Rule + Update the selected association + Clear Selection + Clear the selected executable + + Current Rules + Define per-process rules. Power plans are global; masks and priorities apply to matching processes. + Remove + Remove the selected association + Default Power Plan (fallback when no rule matches) + Power plan to use when no associated processes are running: + Set Default Power Plan + + Automation Monitoring Settings + Enable Event-Based Automation (WMI) + Enable Automation Fallback Polling + Polling Interval (seconds): + Power Plan Change Delay (ms): + Prevent Duplicate Power Plan Changes + Save Configuration + No automation rules yet + Status: + Start Automation Monitoring + Stop Automation Monitoring + + Applied to each matching process when the automation rule runs. + Applied only to matching processes; it does not change the global Windows power plan. + Selected Executable: + Description: + Automation Service + Rules + Executable + Power Plan + CPU Mask + Priority + Description + Process Priority: + + + Process Management + Search, filter, and control active process configurations + Search processes by name + Hide Windows system processes + Hide System Processes + Hide processes with very low CPU usage + Hide Idle Processes + Show only applications with visible windows + Active Apps Only + Lock process list + Pause process list refresh and sorting updates while you work with the current list. Background monitoring and saved-rule auto-apply continue while ThreadPilot is running. + Sort by: + Choose how to sort the process list + + Selected Process + No process selected + Select a process from the table to enable affinity, priority, power plan, and rule actions. + Power Plan / Pending Settings + Choose the power plan to activate manually or include with quick apply + Set Power Plan + Activate the selected Windows power plan + + Pending core mask + Select a mask to stage it. This does not change OS affinity until Apply Affinity is clicked. + Apply Affinity + Apply the pending core mask to the selected process and verify the Windows affinity state + Apply Affinity and Save as Rule + Save current process settings as a rule. These settings will be automatically applied when this process starts in the future. + + Set CPU Priority + Apply the selected priority to the process + Enforce Priority by Registry + Apply priority through Windows registry. Process must be restarted for changes to take effect. This setting persists across system reboots. + Set Priority + Realtime priority is blocked by ThreadPilot because it can make Windows unstable or unresponsive. + Realtime (blocked) + + Disable Idle Server + Prevents the system from entering idle state while this process is running. Useful for games and performance-critical applications. + + Profile / Rule + Enter a profile name to save or load settings + Save Profile + Save current CPU affinity and priority as a reusable profile + Load Profile + Load a previously saved profile + Save Current Settings as Rule + + Last ThreadPilot action: + Refresh + Refresh the process list + Load More + Load more processes + + Process Name + Window Title + CPU Usage + Memory Usage + + Affinity staged in the UI. It is not applied until Apply Affinity is clicked. + Affinity currently reported by Windows for the selected process + Read-only preview of current or pending CPU selection + Open CPU Masks tab to create a new custom mask + Shows whether Hyper-Threading (Intel) or SMT (AMD) is present and active on this system + + Copy Process Info + Open Executable Location + Clear CPU Sets + Apply Pending Settings + Apply the pending affinity and selected power plan to the selected process + Rules and changes are applied by ThreadPilot only when configured. + + Above Normal + Below Normal + High + Idle + Low + Normal + Realtime + Batch + Custom + + Has Window + Window + PID + ID + Memory (MB) + Affinity + Priority + Rules + Name + Set Memory Priority + Very Low + Medium + Refresh Process Info + + + Application Settings + Configure notifications, system tray, and application behavior + Notifications + Enable notifications + Notification Level + All + Warnings/Errors only + Silent + Enable balloon notifications (system tray) + Enable toast notifications (Windows 10+) + Notification Types + Power plan changes + Process monitoring events + Error notifications + Success notifications + Duration (ms): + Test Notification + + System Tray + Show tray icon + Minimize to tray + Close to tray (instead of exit) + Start minimized + Enable pending settings apply from tray + Enable automation monitoring controls from tray + Show detailed tooltips + + Basic Preferences + Autostart + Start with Windows + Automatically start ThreadPilot when Windows starts + Low impact mode in background + Lower ThreadPilot's process priority while minimized or hidden to tray. + + Appearance + Use dark theme + Applies a global Dark/Light theme across all tabs + + Language + 简体中文 (Simplified Chinese) + English (English) + Applies a global display language across the entire application interface. + + Rules & automation + Saved rule auto-apply + Apply saved rules when matching processes start + Applies enabled saved rules while ThreadPilot is running. This does not install a Windows Service and does not use registry/IFEO persistence. + Enable WMI process events + Use Windows Management Instrumentation for process start/stop events + Enable fallback polling + Use polling as backup when WMI process events fail + Polling interval (ms): + Fallback polling (ms): + + Advanced + Enable notification history + Max history items: + Auto-hide notifications + Enable notification sound + Enable debug logging + Enable performance counters + + About + Version: + Copyright © 2026 PrimeBuild + Translator: Ylimhs + License: AGPLv3 + Check for updates + Checks on demand using the official GitHub Releases. + + Reset to Defaults + Export Configuration + Import Configuration + Save Settings + ThreadPilot + + + ThreadPilot Settings + Unsaved Settings + You have unsaved changes. Save before closing, discard the changes, or cancel to keep editing. + Cancel + Discard + Save + + + System Tweaks & Optimizations + Configure Windows system optimizations and performance tweaks + Refresh All + Refresh all tweak states + Toggle this Windows tweak + Status + + + Core Parking + Controls CPU core parking for power management + C-States + Controls CPU C-States for power management + SysMain Service + Windows Superfetch/SysMain service for memory management + Prefetch + Windows Prefetch feature for faster application loading + Power Throttling + Windows Power Throttling for energy efficiency + HPET + High Precision Event Timer for system timing + High Scheduling Category + High scheduling priority for gaming applications + Menu Show Delay + Delay before showing context menus + + + Ready + Refreshing system tweaks... + Last refreshed: {0} + Failed to refresh system tweaks + Refresh failed + Toggling {0}... + {0} enabled successfully + {0} disabled successfully + {0} has been enabled + {0} has been disabled + System Tweak Updated + Failed to toggle {0} + Failed to enable {0} + Failed to disable {0} + System Tweak Failed + Failed to {0} {1} + Error toggling {0} + Error toggling {0}: {1} + enable + disable + enabled + disabled + Enabled + Disabled + Not Available + Loading system tweaks... + System tweaks loaded successfully + + + Open Dashboard + Open Diagnostics + Pause Automation Monitoring + Resume Automation Monitoring + System Status + No process selected + Selected: {0} + Apply Pending Settings to Selected Process + Apply Pending Settings to {0} + 🔋 Power Plans + 📋 Profiles + Settings + Exit + No profiles available + Power Plan: {0} + Automation WMI Error + Automation Active + Automation Disabled + Unknown + 💻 CPU: {0:F1}% | RAM: {1:F1}% | {2} + + + Windows denied this change. The process may require administrator rights or may be protected. + The process appears protected by anti-cheat or process protection. ThreadPilot will not try to bypass it. + Administrator mode may help with normal permission issues, but cannot bypass anti-cheat or protected process restrictions. + This CPU selection cannot be safely represented by legacy affinity APIs on this topology. CPU Sets are required for this selection. + This CPU selection does not match the current CPU topology. Review or recreate the preset. + The process exited before ThreadPilot could apply the change. + Windows CPU Sets are unavailable or rejected this selection. ThreadPilot will use a safe fallback only when possible. + High priority can improve responsiveness for some workloads but may reduce system responsiveness. + Realtime priority is blocked by ThreadPilot because it can make Windows unstable or unresponsive. + Persistent launch-time priority may be supported for normal processes, but it does not bypass protected process or anti-cheat restrictions. + Applies saved rules when a matching process starts. Some protected or anti-cheat processes may reject changes. Administrator mode can help with normal permission issues but cannot bypass protection. + The process appears protected by anti-cheat or process protection. ThreadPilot will not try to override it. + + + Power Plan Changed + Power plan changed from '{0}' to '{1}' + Power plan changed to '{0}' for process '{1}' + Process Monitoring Enabled + Process Monitoring Disabled + CPU Affinity Applied + CPU affinity set for '{0}': {1} + Game Boost Activated + Game Boost mode activated for {0} + Game Boost Deactivated + Game Boost mode deactivated after {0} + Process Monitor Error + Affinity blocked + Priority blocked + + + ThreadPilot requires administrator privileges to manage process affinity and power plans. Would you like to restart the application with administrator privileges? + Administrator Privileges Required + Failed to restart with administrator privileges. Please manually run ThreadPilot as administrator. + Elevation Failed + Running with Administrator privileges + Running with limited privileges + + + Could not query Core Parking value via powercfg + Could not query C-States value via powercfg + Prefetch registry key not found + Power Throttling not available on this system + PriorityControl registry key not found + Desktop registry key not found + diff --git a/Locales/zh-CN.xaml b/Locales/zh-CN.xaml new file mode 100644 index 0000000..3a9ce20 --- /dev/null +++ b/Locales/zh-CN.xaml @@ -0,0 +1,529 @@ + + + + ThreadPilot - 进程与电源计划管理器 + 推荐以管理员权限运行 + ThreadPilot 当前正以受限权限运行。某些功能可能无法使用。 + 需要管理员访问权限以执行以下操作: + • 修改电源计划配置 + • 更改进程 CPU 关联性和优先级 + • 应用系统级优化和调整 + • 管理受保护的进程 + 不提升权限继续 + 请求权限提升 + + + 进程管理 + CPU 掩码 + 电源计划 + 自动化规则 + 性能诊断 + 活动日志 + 系统微调 + 应用设置 + + + 启动时最小化 + 开启此项后,ThreadPilot 会在 Windows 登录时静默启动至系统托盘。 + 推荐 + 不再显示 + 打开设置 + + + ThreadPilot 活动日志 + 显示 ThreadPilot 执行的操作,包括已应用规则、关联性更改、优先级更改、电源计划更改、设置更改、调整、优化以及安全故障消息。 + 搜索: + 类别: + 级别: + 从: + 至: + 刷新 + 清除显示 + 导出活动 + 清理诊断文件 + 打开诊断文件夹 + + + 时间 + 状态 + 类别 + 消息 + 详情 + ID + 复制条目 + 刷新 + 没有要显示的日志条目 + 调整过滤器或刷新日志以加载最近的 ThreadPilot 活动。 + + + 活动详情 + 时间戳: + 状态: + 类别: + 消息: + 详情: + 相关性 ID: + 未选择日志条目 + 文件: + 大小: + 调试诊断 + 最大大小 (MB): + 保留天数 (天): + 保存设置 + + + CPU 掩码 + 创建可复用的 CPU 关联性预设以供每个进程使用。 + 在此处选择掩码仅编辑预设。在将其应用于进程或保存在进程规则中之前,它不会更改 CPU 关联性。 + 新建 + 删除 + 复制掩码 + 掩码信息 + 名称: + 描述: + 已启用 + 默认预设 + 当 ThreadPilot 需要后备掩码或创建针对每个进程的规则时预先选择。它不会自动应用 CPU 关联性。 + 禁用以暂时隐藏此掩码 + 选择 CPU 核心 + 切换 CPU 以编辑此掩码预设。更改将自动保存,且不会影响正在运行的进程。“所有核心”是受保护的默认预设。 + 掩码名称、描述、CPU 选择和选项会自动保存。 + 创建新的 CPU 掩码 + 删除选定的掩码 + CPU 核心 + CPU + + + 可选性能诊断 + 诊断是可选的,仅用于故障排除。对于游戏内覆盖层和详细的性能图表,请使用专用工具。 + 快速提示: + 1. 仅在需要针对性的故障排除快照时才打开诊断。 + 2. 在创建自动化规则之前,仅将热点查看作为提示。 + 3. 故障排除完成后,停止实时指标收集。 + 继续前往诊断 + + 活动进程 + 通过一键规则创建来监控 CPU 和内存的占用大户。 + 按名称搜索进程 + 仅限包含规则的 + 仅限可操作的 + 规则影响 + 基于选择创建/更新规则 + 为选定的热点进程创建或更新自动化规则 + + 稳定性时间线 + 清除历史记录 + 清除显示的指标和时间轴历史记录 + 核心快照 + 单核利用率,用于快速的不平衡检查。 + 切换核心面板 + 切换进程面板 + 开始实时指标 + 为此控制面板启动实时指标收集 + 停止实时指标 + 暂停实时指标收集;后台自动化会单独运行 + 刷新 + 刷新当前的仪表板快照 + + 全局电源计划 + 已用内存 + CPU % + 内存 % + 线程数 + 事件 + 严重性 + 详情 + 时间 + 类别 + 进程 + 选定的热点进程 + 指标 + 进程 + + + 电源计划管理 + 管理活动的 Windows 电源计划并导入自定义 .pow 配置文件 + Windows 电源计划 + 选择要激活的 Windows 电源计划。 + 设为活动 Windows 电源计划 + 将活动的 Windows 电源计划更改为所选计划 + 刷新计划 + 刷新可用电源计划的列表 + 自定义电源计划 + 已准备好导入 Windows 的本地 .pow 文件。 + 导入选中的 .pow + 将所选的自定义 .pow 文件导入 Windows + 添加 .pow 文件 + 向自定义库添加新的自定义电源计划文件 + 删除 + 活动 + + + 规则与自动化 + 自动化监控监视进程的启动/停止事件,并应用配置的电源计划、CPU 掩码和优先级规则。 + 规则编辑器 + 从可执行文件或运行中的进程创建规则,以实现电源计划、CPU 掩码和优先级行为的自动化。 + 可执行文件匹配: + 按路径匹配 + 按完整路径而不是仅按可执行文件名称匹配进程 + 浏览可执行文件 + 点击以从您的计算机选择可执行文件 (.exe) + 选择可执行文件。按路径匹配更精确;按名称匹配适用于任何具有该可执行文件名称的进程。 + 使用运行中的进程: + 选择当前运行的进程以使用其可执行文件 + 使用所选进程 + 使用所选运行中进程的可执行文件 + 规则电源计划 (全局切换): + 当此规则匹配时,Windows 将切换全局活动电源计划。 + 规则 CPU 掩码: + 可选: 选择在该进程启动时要应用的 CPU 掩码 + 规则优先级 + 可选: 选择在该进程启动时要应用的进程优先级 + 关联优先级: + 当有多个匹配时,优先级较高的关联优先使用 + 添加规则 + 更新选定规则 + 更新所选的关联 + 清除选择 + 清除选定的可执行文件 + + 当前规则 + 定义针对每个进程的规则。电源计划是全局的;掩码和优先级适用于匹配的进程。 + 删除 + 删除所选关联 + 默认电源计划 (当无规则匹配时的后备方案) + 当没有关联的进程在运行时要使用的电源计划: + 设置默认电源计划 + + 自动化监控设置 + 启用基于事件的自动化 (WMI) + 启用自动化后备轮询 + 轮询间隔 (秒): + 电源计划切换延迟 (毫秒): + 防止重复更改电源计划 + 保存配置 + 尚无自动化规则 + 状态: + 启动自动化监控 + 停止自动化监控 + + 在运行自动化规则时应用于每个匹配的进程。 + 仅应用于匹配的进程;它不会更改全局 Windows 电源计划。 + 选定的可执行文件: + 描述: + 自动化服务 + 规则 + 可执行文件 + 电源计划 + CPU 掩码 + 优先级 + 描述 + 进程优先级: + + + 系统进程管理 + 搜索、过滤和控制活动的进程配置 + 按名称搜索进程 + 隐藏 Windows 系统进程 + 隐藏系统进程 + 隐藏 CPU 使用率极低的进程 + 隐藏空闲进程 + 仅显示具有可见窗口的应用程序 + 仅限活动应用 + 锁定进程列表 + 在您处理当前列表时暂停进程列表刷新和排序更新。ThreadPilot 运行时,后台监控和保存的规则自动应用仍将继续。 + 排序方式: + 选择如何对进程列表进行排序 + + 选定的进程 + 未选择进程 + 从表格中选择一个进程以启用关联性、优先级、电源计划和规则操作。 + 电源计划 / 待处理设置 + 选择要手动激活的电源计划,或包含在快速应用中 + 设置电源计划 + 激活所选的 Windows 电源计划 + + 待处理的核心掩码 + 选择一个掩码来暂存它。在点击“应用关联性”之前,这不会更改操作系统关联性。 + 应用关联性 + 将待处理的核心掩码应用于选定的进程,并验证 Windows 关联性状态 + 应用关联性并保存为规则 + 将当前进程设置保存为规则。以后此进程启动时将自动应用这些设置。 + + 设置 CPU 优先级 + 将所选优先级应用于进程 + 通过注册表强制执行优先级 + 通过 Windows 注册表应用优先级。必须重新启动进程才能使更改生效。此设置在系统重启后仍然有效。 + 设置优先级 + 实时优先级已被 ThreadPilot 阻止,因为它可能会使 Windows 不稳定或无响应。 + 实时 (已阻止) + + 禁用空闲服务 + 在此进程运行时阻止系统进入空闲状态。适用于游戏和对性能要求严苛的应用程序。 + + 配置文件 / 规则 + 输入配置文件名称以保存或加载设置 + 保存配置文件 + 将当前 CPU 关联性和优先级保存为可复用的配置文件 + 加载配置文件 + 加载以前保存的配置文件 + 保存当前设置为规则 + + 最后一次 ThreadPilot 操作: + 刷新 + 刷新进程列表 + 加载更多 + 加载更多进程 + + 进程名称 + 窗口标题 + CPU 使用率 + 内存使用 + + 关联性已暂存在 UI 中。在点击“应用关联性”之前不会应用。 + Windows 当前报告的所选进程的关联性 + 当前或待处理 CPU 选择的只读预览 + 打开 CPU 掩码选项卡以创建新的自定义掩码 + 显示此系统上是否具有并启用了超线程 (Intel) 或 SMT (AMD) + + 复制进程信息 + 打开可执行文件位置 + 清除 CPU 集 + 应用待处理设置 + 将待处理的关联性和选定的电源计划应用于选定的进程 + ThreadPilot 仅在配置后才应用规则和更改。 + + 高于正常 + 低于正常 + + 空闲 + + 正常 + 实时 + 批处理 + 自定义 + + 有窗口 + 窗口 + PID + ID + 内存 (MB) + 关联性 + 优先级 + 规则 + 名称 + 设置内存优先级 + 极低 + + 刷新进程信息 + + + ThreadPilot 系统设置 + 配置通知、系统托盘和应用程序行为 + 系统通知 + 启用应用通知 + 通知级别 + 全部显示 + 仅警告与错误 + 静音模式 + 启用系统托盘气泡通知 + 启用 Windows 吐司通知 (Windows 10+) + 通知事件类别 + 电源计划变更 + 进程监控事件 + 错误与异常通知 + 操作成功提示 + 持续显示时间 (毫秒): + 测试通知发送 + + 系统托盘栏 + 显示托盘图标 + 最小化到托盘 + 关闭主窗口时最小化到托盘 + 开机启动时最小化 + 允许从托盘菜单应用待处理设置 + 允许从托盘菜单控制自动化监控 + 托盘图标显示详细鼠标提示 + + 基本偏好设置 + 系统开机启动 + 随 Windows 开机启动 + 在 Windows 系统启动时自动开启 ThreadPilot + 后台低系统影响模式 + 在最小化或隐藏至托盘时自动降低 ThreadPilot 的进程优先级。 + + 外观主题 + 启用暗黑主题 + 在所有选项卡上全局应用 亮色/暗色 视觉风格 + + 显示语言 + 简体中文 (Simplified Chinese) + English (English) + 在整个应用程序界面应用全局的语言设置。 + + 规则与自动化 + 保存规则自动应用 + 在匹配的进程启动时自动应用已保存规则 + 在 ThreadPilot 运行时自动应用已启用的保存规则。这不会注册 Windows 服务,也不使用注册表/IFEO 进行常驻。 + 启用 WMI 进程启动监控 + 使用 Windows Management Instrumentation 来监听进程启动/退出事件 + 启用后备轮询监控 + 如果 WMI 进程事件订阅失败,自动启用定时轮询作为备份 + 轮询间隔时间 (毫秒): + 后备轮询间隔时间 (毫秒): + + 高级与调试设置 + 启用通知历史记录 + 最大历史条目数: + 自动渐隐通知气泡 + 播放提示音 + 开启调试诊断日志记录 + 启用内部性能计数器 + + 关于应用 + 软件版本: + 版权所有 © 2026 PrimeBuild + 翻译人员:Ylimhs + 软件授权: AGPLv3 + 检查新版本 + 按需通过官方 GitHub Releases API 检索最新稳定版本。 + + 重置为默认值 + 导出配置包 + 导入配置包 + 保存并应用设置 + ThreadPilot + + + ThreadPilot 设置 + 未保存的设置 + 您有尚未保存的更改。请选择在关闭前保存、放弃修改或取消并返回编辑。 + 取消 + 放弃更改 + 保存 + + + 系统微调与优化 + 配置 Windows 系统级优化项和性能微调选项 + 刷新状态 + 刷新所有微调选项的状态 + 切换此 Windows 系统微调项 + 当前状态 + + + 核心休眠 (Core Parking) + 控制 CPU 核心休眠以进行电源管理 + C-States 状态 + 控制 CPU C-States 以进行节能电源管理 + SysMain 服务 + Windows Superfetch/SysMain 服务,用于内存管理 + 预取 (Prefetch) + Windows Prefetch 功能,用于加速应用程序加载 + 电源限流 (Power Throttling) + Windows 电源限流功能,用于能效优化 + HPET 定时器 + 高精度事件计时器,用于系统精准定时 + 高调度优先级类别 + 为游戏应用程序提供高调度优先级 + 菜单显示延迟 + 显示上下文菜单之前的延迟时间 + + + 就绪 + 正在刷新系统微调选项... + 上次刷新时间: {0} + 刷新系统微调失败 + 刷新失败 + 正在切换 {0}... + {0} 已成功启用 + {0} 已成功禁用 + {0} 已启用 + {0} 已禁用 + 系统微调已更新 + 无法切换 {0} + 未能启用 {0} + 未能禁用 {0} + 系统微调失败 + 未能{0}{1} + 切换 {0} 出错 + 切换 {0} 时出错: {1} + 启用 + 禁用 + 启用 + 禁用 + 已启用 + 已禁用 + 不可用 + 正在加载系统微调... + 系统微调已成功加载 + + + 打开主控制面板 + 打开性能诊断 + 暂停自动化监控 + 恢复自动化监控 + 系统状态 + 未选择进程 + 已选择: {0} + 应用待处理设置到所选进程 + 应用待处理设置到 {0} + 🔋 电源计划 + 📋 配置文件 + 应用设置 + 退出程序 + 无可用配置文件 + 电源计划: {0} + 自动化 WMI 错误 + 自动化监控已启用 + 自动化监控已禁用 + 未知 + 💻 CPU: {0:F1}% | 内存: {1:F1}% | {2} + + + Windows 拒绝了此更改。该进程可能需要管理员权限或受保护。 + 该进程似乎受到反作弊系统或进程保护的保护。ThreadPilot 不会尝试绕过它。 + 管理员模式可能有助于解决普通的权限问题,但无法绕过反作弊系统或受保护的进程限制。 + 在此拓扑上,此 CPU 选择无法由传统关联性 API 安全地表示。此选择需要使用 CPU 集。 + 此 CPU 选择与当前 CPU 拓扑不匹配。请检查或重新创建预设。 + 在 ThreadPilot 应用更改之前,进程已退出。 + Windows CPU 集不可用或拒绝了此选择。ThreadPilot 将仅在可能时使用 safe 后备方案。 + 高优先级可以提高某些工作负载的响应速度,但可能会降低系统整体响应速度。 + 实时优先级已被 ThreadPilot 阻止,因为它可能会使 Windows 不稳定或无响应。 + 普通进程可能支持持久的启动时优先级,但这不会绕过受保护进程或反作弊系统的限制。 + 在匹配的进程启动时应用保存的规则。某些受保护或反作弊进程可能会拒绝更改。管理员模式有助于解决普通权限问题,但无法绕过进程保护。 + 该进程似乎受到反作弊系统或进程保护的保护。ThreadPilot 不会尝试对其进行改写。 + + + 电源计划已更改 + 电源计划已从“{0}”更改为“{1}” + 已为进程“{1}”将电源计划切换为“{0}” + 进程监控已启用 + 进程监控已禁用 + CPU 关联性已应用 + 已为“{0}”设置 CPU 关联性: {1} + 游戏加速已启用 + 已为 {0} 启用游戏加速模式 + 游戏加速已关闭 + 游戏加速模式已关闭,持续时间: {0} + 进程监控器错误 + 关联性应用被阻止 + 优先级应用被阻止 + + + ThreadPilot 需要管理员权限来管理进程关联性和电源计划。 您想以管理员权限重新启动应用程序吗? + 需要管理员权限 + 无法以管理员权限重新启动。请手动以管理员身份运行 ThreadPilot。 + 权限提升失败 + 以管理员权限运行中 + 以受限权限运行中 + + + 无法通过 powercfg 查询核心停车值 + 无法通过 powercfg 查询 C-States 值 + 未找到预取注册表键 + 此系统上不支持电源限流 + 未找到 PriorityControl 注册表键 + 未找到 Desktop 注册表键 + diff --git a/Models/ApplicationSettingsModel.cs b/Models/ApplicationSettingsModel.cs index 1875e4e..40e6c78 100644 --- a/Models/ApplicationSettingsModel.cs +++ b/Models/ApplicationSettingsModel.cs @@ -151,6 +151,9 @@ public partial class ApplicationSettingsModel : ObservableObject, IModel [ObservableProperty] private bool hasUserThemePreference = false; + [ObservableProperty] + private string language = LocalizationService.DefaultLanguage; + // Monitoring Settings [ObservableProperty] private int pollingIntervalMs = 5000; @@ -249,6 +252,7 @@ public void CopyFrom(ApplicationSettingsModel other) this.ClearMasksOnClose = other.ClearMasksOnClose; this.UseDarkTheme = other.UseDarkTheme; this.HasUserThemePreference = other.HasUserThemePreference; + this.Language = LocalizationService.NormalizeLanguage(other.Language); // Monitoring Settings this.PollingIntervalMs = other.PollingIntervalMs; diff --git a/Services/ApplicationSettingsService.cs b/Services/ApplicationSettingsService.cs index dc7a7f5..02d52b7 100644 --- a/Services/ApplicationSettingsService.cs +++ b/Services/ApplicationSettingsService.cs @@ -246,16 +246,18 @@ public void ValidateAndFixSettings() this.settings.MaxNotificationHistoryItems = 1000; } - // Validate custom icon path - if (this.settings.UseCustomTrayIcon && !string.IsNullOrEmpty(this.settings.CustomTrayIconPath)) - { - if (!File.Exists(this.settings.CustomTrayIconPath)) - { + // Validate custom icon path + if (this.settings.UseCustomTrayIcon && !string.IsNullOrEmpty(this.settings.CustomTrayIconPath)) + { + if (!File.Exists(this.settings.CustomTrayIconPath)) + { this.logger.LogWarning("Custom tray icon file not found: {Path}", this.settings.CustomTrayIconPath); - this.settings.UseCustomTrayIcon = false; - } - } - } + this.settings.UseCustomTrayIcon = false; + } + } + + this.settings.Language = LocalizationService.NormalizeLanguage(this.settings.Language); + } public async Task ExportSettingsAsync(string filePath) { diff --git a/Services/ILocalizationService.cs b/Services/ILocalizationService.cs new file mode 100644 index 0000000..5361e75 --- /dev/null +++ b/Services/ILocalizationService.cs @@ -0,0 +1,44 @@ +/* + * ThreadPilot - Advanced Windows Process and Power Plan Manager + * Copyright (C) 2025 Prime Build + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace ThreadPilot.Services +{ + /// + /// Service for managing application localization and display language. + /// + public interface ILocalizationService + { + /// + /// Gets the current display language. + /// + string CurrentLanguage { get; } + + /// + /// Event fired when the active language changes. + /// + event EventHandler? LanguageChanged; + + /// + /// Applies the specified display language. + /// + void ApplyLanguage(string? language); + + /// + /// Gets the localized string for the specified key. + /// + string GetString(string key); + } +} diff --git a/Services/LocalizationService.cs b/Services/LocalizationService.cs new file mode 100644 index 0000000..9e3270b --- /dev/null +++ b/Services/LocalizationService.cs @@ -0,0 +1,269 @@ +/* + * ThreadPilot - Advanced Windows Process and Power Plan Manager + * Copyright (C) 2025 Prime Build + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, version 3 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +namespace ThreadPilot.Services +{ + using System; + using System.Collections.Generic; + using System.Windows; + using Microsoft.Extensions.Logging; + + /// + /// Service for managing application localization and display language. + /// + public class LocalizationService : ILocalizationService + { + public const string DefaultLanguage = "en-US"; + public const string SimplifiedChineseLanguage = "zh-CN"; + + private const string EnUsDictionaryPath = "Locales/en-US.xaml"; + private const string ZhCnDictionaryPath = "Locales/zh-CN.xaml"; + + private readonly ILogger logger; + private readonly IReadOnlyDictionary? englishStrings; + private readonly IReadOnlyDictionary? chineseStrings; + private ResourceDictionary? activeLocaleDictionary; + private ResourceDictionary? englishFallbackDictionary; + private Uri? activeLocaleUri; + + public string CurrentLanguage { get; private set; } = DefaultLanguage; + + public event EventHandler? LanguageChanged; + + public LocalizationService(ILogger logger) + : this(logger, englishStrings: null, chineseStrings: null) + { + } + + public LocalizationService( + ILogger logger, + IReadOnlyDictionary? englishStrings, + IReadOnlyDictionary? chineseStrings) + { + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + this.englishStrings = englishStrings; + this.chineseStrings = chineseStrings; + } + + public static string NormalizeLanguage(string? language) + { + if (string.Equals(language, SimplifiedChineseLanguage, StringComparison.OrdinalIgnoreCase)) + { + return SimplifiedChineseLanguage; + } + + return DefaultLanguage; + } + + public void ApplyLanguage(string? language) + { + var normalizedLanguage = NormalizeLanguage(language); + var targetUri = new Uri(GetDictionaryPath(normalizedLanguage), UriKind.Relative); + + this.CurrentLanguage = normalizedLanguage; + + var appResources = System.Windows.Application.Current?.Resources; + if (appResources == null) + { + this.activeLocaleUri = targetUri; + this.LanguageChanged?.Invoke(this, normalizedLanguage); + return; + } + + try + { + this.ApplyLanguageDictionary(appResources, targetUri); + this.activeLocaleUri = targetUri; + this.logger.LogInformation("Applied display language {Language}", normalizedLanguage); + this.LanguageChanged?.Invoke(this, normalizedLanguage); + } + catch (Exception ex) + { + this.logger.LogError(ex, "Failed to apply language {Language}", normalizedLanguage); + } + } + + public string GetString(string key) + { + if (string.IsNullOrWhiteSpace(key)) + { + return string.Empty; + } + + if (this.TryGetStringFromOverrides(this.CurrentLanguage, key, out var localized)) + { + return localized; + } + + if (this.TryGetStringFromApplicationResources(key, out localized)) + { + return localized; + } + + if (this.activeLocaleDictionary != null && TryGetString(this.activeLocaleDictionary, key, out localized)) + { + return localized; + } + + if (this.CurrentLanguage != DefaultLanguage && + this.TryGetStringFromOverrides(DefaultLanguage, key, out localized)) + { + return localized; + } + + if (this.CurrentLanguage != DefaultLanguage && + this.TryGetStringFromEnglishFallbackDictionary(key, out localized)) + { + return localized; + } + + return key; + } + + private void ApplyLanguageDictionary(ResourceDictionary appResources, Uri targetUri) + { + ResourceDictionary? matchingDictionary = null; + for (var i = appResources.MergedDictionaries.Count - 1; i >= 0; i--) + { + var dictionary = appResources.MergedDictionaries[i]; + var source = dictionary.Source?.OriginalString; + if (IsLocaleDictionary(source)) + { + if (matchingDictionary == null && + string.Equals(source, targetUri.OriginalString, StringComparison.OrdinalIgnoreCase)) + { + matchingDictionary = dictionary; + continue; + } + + appResources.MergedDictionaries.RemoveAt(i); + } + } + + if (matchingDictionary != null) + { + appResources.MergedDictionaries.Remove(matchingDictionary); + appResources.MergedDictionaries.Insert(0, matchingDictionary); + this.activeLocaleDictionary = matchingDictionary; + } + else + { + var nextDictionary = new ResourceDictionary { Source = targetUri }; + appResources.MergedDictionaries.Insert(0, nextDictionary); + this.activeLocaleDictionary = nextDictionary; + } + } + + private static string GetDictionaryPath(string language) + { + return language == SimplifiedChineseLanguage ? ZhCnDictionaryPath : EnUsDictionaryPath; + } + + private static bool IsLocaleDictionary(string? source) + { + return !string.IsNullOrWhiteSpace(source) && + (source.EndsWith(EnUsDictionaryPath, StringComparison.OrdinalIgnoreCase) || + source.EndsWith(ZhCnDictionaryPath, StringComparison.OrdinalIgnoreCase)); + } + + private static bool TryGetString(ResourceDictionary dictionary, string key, out string value) + { + if (dictionary.Contains(key) && dictionary[key] is string text && !string.IsNullOrEmpty(text)) + { + value = text; + return true; + } + + value = string.Empty; + return false; + } + + private bool TryGetStringFromOverrides(string language, string key, out string value) + { + var source = language == SimplifiedChineseLanguage ? this.chineseStrings : this.englishStrings; + if (source != null && source.TryGetValue(key, out var text) && !string.IsNullOrEmpty(text)) + { + value = text; + return true; + } + + value = string.Empty; + return false; + } + + private bool TryGetStringFromApplicationResources(string key, out string value) + { + value = string.Empty; + var app = System.Windows.Application.Current; + if (app == null) + { + return false; + } + + try + { + if (app.Dispatcher.CheckAccess()) + { + return TryGetApplicationResourceValue(app, key, out value); + } + + var found = false; + var dispatcherValue = string.Empty; + app.Dispatcher.Invoke(() => + { + found = TryGetApplicationResourceValue(app, key, out dispatcherValue); + }); + value = dispatcherValue; + return found; + } + catch (Exception ex) + { + this.logger.LogDebug(ex, "Failed to read localized resource {Key}", key); + return false; + } + } + + private static bool TryGetApplicationResourceValue(System.Windows.Application app, string key, out string value) + { + if (app.Resources.Contains(key) && app.Resources[key] is string text && !string.IsNullOrEmpty(text)) + { + value = text; + return true; + } + + value = string.Empty; + return false; + } + + private bool TryGetStringFromEnglishFallbackDictionary(string key, out string value) + { + value = string.Empty; + try + { + this.englishFallbackDictionary ??= new ResourceDictionary + { + Source = new Uri(EnUsDictionaryPath, UriKind.Relative), + }; + return TryGetString(this.englishFallbackDictionary, key, out value); + } + catch (Exception ex) + { + this.logger.LogDebug(ex, "Failed to load English fallback localization dictionary"); + return false; + } + } + } +} diff --git a/Services/NotificationService.cs b/Services/NotificationService.cs index 7ba56c6..3298302 100644 --- a/Services/NotificationService.cs +++ b/Services/NotificationService.cs @@ -35,6 +35,7 @@ public class NotificationService : INotificationService, IDisposable private readonly ILogger logger; private readonly IApplicationSettingsService settingsService; private readonly ISystemTrayService systemTrayService; + private readonly ILocalizationService localizationService; private readonly List notificationHistory; private ApplicationSettingsModel settings; private bool disposed = false; @@ -50,11 +51,13 @@ public class NotificationService : INotificationService, IDisposable public NotificationService( ILogger logger, IApplicationSettingsService settingsService, - ISystemTrayService systemTrayService) + ISystemTrayService systemTrayService, + ILocalizationService localizationService) { this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService)); this.systemTrayService = systemTrayService ?? throw new ArgumentNullException(nameof(systemTrayService)); + this.localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); this.notificationHistory = new List(); this.settings = this.settingsService.Settings; @@ -102,6 +105,9 @@ public async Task ShowNotificationAsync(NotificationModel notification) try { + notification.Title = this.TryGetLocalizedNotificationString(notification.Title); + notification.Message = this.TryGetLocalizedNotificationString(notification.Message); + // Check if notifications are enabled if (!this.AreNotificationsEnabled(notification.Type)) { @@ -176,11 +182,18 @@ public async Task ShowPowerPlanChangeNotificationAsync(string oldPlan, string ne return; } + var title = this.GetLocalizedString("Notification_PowerPlanChangedTitle"); var message = string.IsNullOrEmpty(processName) - ? $"Power plan changed from '{oldPlan}' to '{newPlan}'" - : $"Power plan changed to '{newPlan}' for process '{processName}'"; + ? string.Format( + this.GetLocalizedString("Notification_PowerPlanChangedFormat"), + oldPlan, + newPlan) + : string.Format( + this.GetLocalizedString("Notification_PowerPlanChangedProcessFormat"), + newPlan, + processName); - var notification = new NotificationModel("Power Plan Changed", message, NotificationType.PowerPlanChange) + var notification = new NotificationModel(title, message, NotificationType.PowerPlanChange) { Category = "PowerPlan", SourceService = "PowerPlanService", @@ -197,7 +210,9 @@ public async Task ShowProcessMonitoringNotificationAsync(string message, bool is return; } - var title = isEnabled ? "Process Monitoring Enabled" : "Process Monitoring Disabled"; + var title = isEnabled + ? this.GetLocalizedString("Notification_ProcessMonitoringEnabled") + : this.GetLocalizedString("Notification_ProcessMonitoringDisabled"); var type = isEnabled ? NotificationType.Success : NotificationType.Warning; var notification = new NotificationModel(title, message, type) @@ -212,9 +227,15 @@ public async Task ShowProcessMonitoringNotificationAsync(string message, bool is public async Task ShowCpuAffinityNotificationAsync(string processName, string affinityInfo) { + var title = this.GetLocalizedString("Notification_CpuAffinityAppliedTitle"); + var message = string.Format( + this.GetLocalizedString("Notification_CpuAffinityAppliedFormat"), + processName, + affinityInfo); + var notification = new NotificationModel( - "CPU Affinity Applied", - $"CPU affinity set for '{processName}': {affinityInfo}", + title, + message, NotificationType.CpuAffinity) { Category = "CpuAffinity", @@ -396,6 +417,63 @@ private void OnSettingsChanged(object? sender, ApplicationSettingsChangedEventAr this.UpdateSettings(e.NewSettings); } + private string GetLocalizedString(string key) + { + var localized = this.localizationService.GetString(key); + return string.IsNullOrEmpty(localized) ? key : localized; + } + + private string TryGetLocalizedNotificationString(string input) + { + if (string.IsNullOrEmpty(input)) + { + return input; + } + + var key = input switch + { + "Game Boost Activated" => "Notification_GameBoostActivatedTitle", + "Game Boost Deactivated" => "Notification_GameBoostDeactivatedTitle", + "Process Monitor Error" => "Notification_ProcessMonitorErrorTitle", + "Affinity blocked" => "Notification_AffinityBlockedTitle", + "Priority blocked" => "Notification_PriorityBlockedTitle", + _ => null, + }; + + if (key != null) + { + var localized = this.GetLocalizedString(key); + if (!string.Equals(localized, key, StringComparison.Ordinal)) + { + return localized; + } + } + + const string GameBoostActivatedPrefix = "Game Boost mode activated for "; + if (input.StartsWith(GameBoostActivatedPrefix, StringComparison.OrdinalIgnoreCase)) + { + var processName = input[GameBoostActivatedPrefix.Length..]; + var format = this.GetLocalizedString("Notification_GameBoostActivatedFormat"); + if (!string.Equals(format, "Notification_GameBoostActivatedFormat", StringComparison.Ordinal)) + { + return string.Format(format, processName); + } + } + + const string GameBoostDeactivatedPrefix = "Game Boost mode deactivated after "; + if (input.StartsWith(GameBoostDeactivatedPrefix, StringComparison.OrdinalIgnoreCase)) + { + var duration = input[GameBoostDeactivatedPrefix.Length..]; + var format = this.GetLocalizedString("Notification_GameBoostDeactivatedFormat"); + if (!string.Equals(format, "Notification_GameBoostDeactivatedFormat", StringComparison.Ordinal)) + { + return string.Format(format, duration); + } + } + + return input; + } + public void Dispose() { if (this.disposed) @@ -416,4 +494,3 @@ public void Dispose() } } } - diff --git a/Services/ServiceConfiguration.cs b/Services/ServiceConfiguration.cs index 1bf3426..23dc385 100644 --- a/Services/ServiceConfiguration.cs +++ b/Services/ServiceConfiguration.cs @@ -165,6 +165,7 @@ private static IServiceCollection ConfigureApplicationLevelServices(this IServic // Application configuration and settings services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); // User interface services diff --git a/Tests/ThreadPilot.Core.Tests/ApplicationSettingsModelTests.cs b/Tests/ThreadPilot.Core.Tests/ApplicationSettingsModelTests.cs index 1fd0f35..214ac48 100644 --- a/Tests/ThreadPilot.Core.Tests/ApplicationSettingsModelTests.cs +++ b/Tests/ThreadPilot.Core.Tests/ApplicationSettingsModelTests.cs @@ -13,6 +13,21 @@ public void Constructor_StartMinimizedDefaultsFalse_ForManualLaunchVisibility() Assert.False(settings.StartMinimized); Assert.True(settings.ApplyPersistentRulesOnProcessStart); Assert.False(settings.HasSeenStartupMinimizedSuggestion); + Assert.Equal("en-US", settings.Language); + } + + [Fact] + public void CopyFrom_CopiesLanguage() + { + var source = new ApplicationSettingsModel + { + Language = "zh-CN", + }; + var target = new ApplicationSettingsModel(); + + target.CopyFrom(source); + + Assert.Equal("zh-CN", target.Language); } [Fact] diff --git a/Tests/ThreadPilot.Core.Tests/ApplicationSettingsServiceTests.cs b/Tests/ThreadPilot.Core.Tests/ApplicationSettingsServiceTests.cs index cd9589f..eb712c4 100644 --- a/Tests/ThreadPilot.Core.Tests/ApplicationSettingsServiceTests.cs +++ b/Tests/ThreadPilot.Core.Tests/ApplicationSettingsServiceTests.cs @@ -26,6 +26,7 @@ public async Task LoadSettingsAsync_CreatesDefaults_WhenFileIsMissing() Assert.False(service.Settings.EnableSelfAffinityLimit); Assert.True(service.Settings.AutostartWithWindows); Assert.False(service.Settings.StartMinimized); + Assert.Equal("en-US", service.Settings.Language); } [Fact] @@ -148,6 +149,42 @@ public async Task LoadSettingsAsync_PreservesStartupMinimizedSuggestionDismissal Assert.True(service.Settings.HasSeenStartupMinimizedSuggestion); } + [Fact] + public async Task LoadSettingsAsync_PreservesSupportedLanguage() + { + var storage = new FakeSettingsStorage(); + storage.Files[TestPaths.SettingsFilePath] = """ + { + "language": "zh-CN" + } + """; + var service = CreateService(storage); + + await service.LoadSettingsAsync(); + + Assert.Equal("zh-CN", service.Settings.Language); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData("fr-FR")] + [InlineData("zh")] + public async Task LoadSettingsAsync_FallsBackToEnglish_WhenLanguageIsInvalid(string language) + { + var storage = new FakeSettingsStorage(); + storage.Files[TestPaths.SettingsFilePath] = $$""" + { + "language": "{{language}}" + } + """; + var service = CreateService(storage); + + await service.LoadSettingsAsync(); + + Assert.Equal("en-US", service.Settings.Language); + } + [Fact] public async Task ImportSettingsAsync_Throws_WhenFileIsMissing() { diff --git a/Tests/ThreadPilot.Core.Tests/LocalizationServiceTests.cs b/Tests/ThreadPilot.Core.Tests/LocalizationServiceTests.cs new file mode 100644 index 0000000..4f1d0d5 --- /dev/null +++ b/Tests/ThreadPilot.Core.Tests/LocalizationServiceTests.cs @@ -0,0 +1,200 @@ +namespace ThreadPilot.Core.Tests +{ + using System.Reflection; + using System.Windows; + using Microsoft.Extensions.Logging.Abstractions; + using ThreadPilot.Services; + + public sealed class LocalizationServiceTests + { + [Fact] + public void Constructor_DefaultsToEnglish() + { + var service = CreateService(); + + Assert.Equal("en-US", service.CurrentLanguage); + } + + [Fact] + public void ApplyLanguage_AppliesChinese_WhenSupported() + { + var service = CreateService(); + + service.ApplyLanguage("zh-CN"); + + Assert.Equal("zh-CN", service.CurrentLanguage); + } + + [Fact] + public void ApplyLanguage_FiresLanguageChangedWithNormalizedLanguage() + { + var service = CreateService(); + var observedLanguages = new List(); + service.LanguageChanged += (_, language) => observedLanguages.Add(language); + + service.ApplyLanguage("zh-cn"); + service.ApplyLanguage("unsupported"); + + Assert.Equal(new[] { "zh-CN", "en-US" }, observedLanguages); + } + + [Fact] + public void ApplyLanguage_RemovesDuplicateAndOldLocaleDictionaries() + { + var resources = new ResourceDictionary(); + var nonLocaleDictionary = CreateDictionaryWithSource("Themes/FluentDark.xaml"); + var oldEnglishDictionary = CreateDictionaryWithSource("Locales/en-US.xaml"); + var duplicateChineseDictionary = CreateDictionaryWithSource("Locales/zh-CN.xaml"); + var matchingChineseDictionary = CreateDictionaryWithSource("Locales/zh-CN.xaml"); + resources.MergedDictionaries.Add(nonLocaleDictionary); + resources.MergedDictionaries.Add(oldEnglishDictionary); + resources.MergedDictionaries.Add(duplicateChineseDictionary); + resources.MergedDictionaries.Add(matchingChineseDictionary); + var service = CreateService(); + + InvokeApplyLanguageDictionary(service, resources, new Uri("Locales/zh-CN.xaml", UriKind.Relative)); + + Assert.Equal(2, resources.MergedDictionaries.Count); + Assert.Same(matchingChineseDictionary, resources.MergedDictionaries[0]); + Assert.Same(nonLocaleDictionary, resources.MergedDictionaries[1]); + Assert.DoesNotContain(resources.MergedDictionaries, dictionary => ReferenceEquals(dictionary, oldEnglishDictionary)); + Assert.DoesNotContain(resources.MergedDictionaries, dictionary => ReferenceEquals(dictionary, duplicateChineseDictionary)); + } + + [Fact] + public void GetString_UsesCurrentLanguageOverrideBeforeEnglishFallback() + { + var service = CreateService( + new Dictionary + { + ["Shared_Key"] = "English", + }, + new Dictionary + { + ["Shared_Key"] = "Chinese", + }); + service.ApplyLanguage("zh-CN"); + + var result = service.GetString("Shared_Key"); + + Assert.Equal("Chinese", result); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData(" ")] + [InlineData("fr-FR")] + [InlineData("zh")] + public void ApplyLanguage_FallsBackToEnglish_WhenLanguageIsInvalid(string? language) + { + var service = CreateService(); + service.ApplyLanguage("zh-CN"); + + service.ApplyLanguage(language); + + Assert.Equal("en-US", service.CurrentLanguage); + } + + [Fact] + public void GetString_UsesEnglishFallback_WhenActiveLanguageMissesKey() + { + var service = CreateService( + new Dictionary + { + ["Shared_Key"] = "English fallback", + }, + new Dictionary()); + service.ApplyLanguage("zh-CN"); + + var result = service.GetString("Shared_Key"); + + Assert.Equal("English fallback", result); + } + + [Fact] + public void GetString_ReturnsKey_WhenNoTranslationExists() + { + var service = CreateService(); + + var result = service.GetString("Missing_Key"); + + Assert.Equal("Missing_Key", result); + } + + [Fact] + public void GetString_ReturnsEmpty_WhenKeyIsBlank() + { + var service = CreateService(); + + Assert.Equal(string.Empty, service.GetString(string.Empty)); + Assert.Equal(string.Empty, service.GetString(" ")); + } + + [Fact] + public void LocaleFiles_DefineEnglishDefaultAndOptionalChineseLanguageLabels() + { + var root = FindRepositoryRoot(); + var english = File.ReadAllText(Path.Combine(root, "Locales", "en-US.xaml")); + var chinese = File.ReadAllText(Path.Combine(root, "Locales", "zh-CN.xaml")); + var appXaml = File.ReadAllText(Path.Combine(root, "App.xaml")); + + Assert.Contains("Source=\"Locales/en-US.xaml\"", appXaml, StringComparison.Ordinal); + Assert.DoesNotContain("Source=\"Locales/zh-CN.xaml\"", appXaml, StringComparison.Ordinal); + Assert.Contains("x:Key=\"SettingsView_LanguageEnUs\"", english, StringComparison.Ordinal); + Assert.Contains("x:Key=\"SettingsView_LanguageZhCn\"", english, StringComparison.Ordinal); + Assert.Contains("x:Key=\"SettingsView_LanguageEnUs\"", chinese, StringComparison.Ordinal); + Assert.Contains("x:Key=\"SettingsView_LanguageZhCn\"", chinese, StringComparison.Ordinal); + } + + private static LocalizationService CreateService( + IReadOnlyDictionary? englishStrings = null, + IReadOnlyDictionary? chineseStrings = null) + { + return new LocalizationService( + NullLogger.Instance, + englishStrings, + chineseStrings); + } + + private static string FindRepositoryRoot() + { + var directory = new DirectoryInfo(AppContext.BaseDirectory); + while (directory != null && !File.Exists(Path.Combine(directory.FullName, "ThreadPilot_1.sln"))) + { + directory = directory.Parent; + } + + return directory?.FullName ?? throw new InvalidOperationException("Repository root was not found."); + } + + private static ResourceDictionary CreateDictionaryWithSource(string source) + { + var dictionary = new ResourceDictionary(); + var sourceField = typeof(ResourceDictionary).GetField("_source", BindingFlags.Instance | BindingFlags.NonPublic); + if (sourceField == null) + { + throw new InvalidOperationException("ResourceDictionary source field was not found."); + } + + sourceField.SetValue(dictionary, new Uri(source, UriKind.Relative)); + return dictionary; + } + + private static void InvokeApplyLanguageDictionary( + LocalizationService service, + ResourceDictionary resources, + Uri targetUri) + { + var method = typeof(LocalizationService).GetMethod( + "ApplyLanguageDictionary", + BindingFlags.Instance | BindingFlags.NonPublic); + if (method == null) + { + throw new InvalidOperationException("ApplyLanguageDictionary method was not found."); + } + + method.Invoke(service, new object[] { resources, targetUri }); + } + } +} diff --git a/Tests/ThreadPilot.Core.Tests/NotificationServiceLocalizationTests.cs b/Tests/ThreadPilot.Core.Tests/NotificationServiceLocalizationTests.cs new file mode 100644 index 0000000..0383448 --- /dev/null +++ b/Tests/ThreadPilot.Core.Tests/NotificationServiceLocalizationTests.cs @@ -0,0 +1,161 @@ +namespace ThreadPilot.Core.Tests +{ + using Microsoft.Extensions.Logging.Abstractions; + using Moq; + using ThreadPilot.Models; + using ThreadPilot.Services; + + public sealed class NotificationServiceLocalizationTests + { + [Fact] + public async Task ShowPowerPlanChangeNotificationAsync_UsesLocalizedTitleAndFormat() + { + var harness = new Harness(new Dictionary + { + ["Notification_PowerPlanChangedTitle"] = "Localized power title", + ["Notification_PowerPlanChangedFormat"] = "Changed {0} -> {1}", + }); + var service = harness.CreateService(); + + await service.ShowPowerPlanChangeNotificationAsync("Balanced", "Performance"); + + var notification = Assert.Single(service.NotificationHistory); + Assert.Equal("Localized power title", notification.Title); + Assert.Equal("Changed Balanced -> Performance", notification.Message); + harness.Tray.Verify( + tray => tray.ShowTrayNotification( + "Localized power title", + "Changed Balanced -> Performance", + NotificationType.PowerPlanChange, + It.IsAny()), + Times.Once); + } + + [Fact] + public async Task ShowPowerPlanChangeNotificationAsync_UsesLocalizedProcessFormat() + { + var harness = new Harness(new Dictionary + { + ["Notification_PowerPlanChangedTitle"] = "Power", + ["Notification_PowerPlanChangedProcessFormat"] = "{1}: {0}", + }); + var service = harness.CreateService(); + + await service.ShowPowerPlanChangeNotificationAsync("Balanced", "Performance", "game.exe"); + + var notification = Assert.Single(service.NotificationHistory); + Assert.Equal("Power", notification.Title); + Assert.Equal("game.exe: Performance", notification.Message); + } + + [Theory] + [InlineData(true, "Enabled localized", NotificationType.Success)] + [InlineData(false, "Disabled localized", NotificationType.Warning)] + public async Task ShowProcessMonitoringNotificationAsync_UsesLocalizedTitle(bool isEnabled, string expectedTitle, NotificationType expectedType) + { + var harness = new Harness(new Dictionary + { + ["Notification_ProcessMonitoringEnabled"] = "Enabled localized", + ["Notification_ProcessMonitoringDisabled"] = "Disabled localized", + }); + var service = harness.CreateService(); + + await service.ShowProcessMonitoringNotificationAsync("Monitoring changed", isEnabled); + + var notification = Assert.Single(service.NotificationHistory); + Assert.Equal(expectedTitle, notification.Title); + Assert.Equal("Monitoring changed", notification.Message); + Assert.Equal(expectedType, notification.Type); + } + + [Fact] + public async Task ShowCpuAffinityNotificationAsync_UsesLocalizedTitleAndFormat() + { + var harness = new Harness(new Dictionary + { + ["Notification_CpuAffinityAppliedTitle"] = "Affinity localized", + ["Notification_CpuAffinityAppliedFormat"] = "{0} uses {1}", + }); + var service = harness.CreateService(); + + await service.ShowCpuAffinityNotificationAsync("game.exe", "CPU 0, 1"); + + var notification = Assert.Single(service.NotificationHistory); + Assert.Equal("Affinity localized", notification.Title); + Assert.Equal("game.exe uses CPU 0, 1", notification.Message); + } + + [Fact] + public async Task ShowNotificationAsync_LocalizesKnownAndDynamicGameBoostStrings() + { + var harness = new Harness(new Dictionary + { + ["Notification_GameBoostActivatedTitle"] = "Boost title", + ["Notification_GameBoostActivatedFormat"] = "Boosted {0}", + }); + var service = harness.CreateService(); + + await service.ShowNotificationAsync( + "Game Boost Activated", + "Game Boost mode activated for game.exe", + NotificationType.Information); + + var notification = Assert.Single(service.NotificationHistory); + Assert.Equal("Boost title", notification.Title); + Assert.Equal("Boosted game.exe", notification.Message); + } + + [Fact] + public async Task ShowNotificationAsync_KeepsOriginalText_WhenLocalizationKeyIsMissing() + { + var harness = new Harness(new Dictionary()); + var service = harness.CreateService(); + + await service.ShowNotificationAsync( + "Affinity blocked", + "Unmapped notification message", + NotificationType.Warning); + + var notification = Assert.Single(service.NotificationHistory); + Assert.Equal("Affinity blocked", notification.Title); + Assert.Equal("Unmapped notification message", notification.Message); + } + + private sealed class Harness + { + private readonly IReadOnlyDictionary localizedStrings; + + public Mock Settings { get; } = new(MockBehavior.Loose); + + public Mock Tray { get; } = new(MockBehavior.Loose); + + public Mock Localization { get; } = new(MockBehavior.Loose); + + public Harness(IReadOnlyDictionary localizedStrings) + { + this.localizedStrings = localizedStrings; + this.Settings.SetupGet(service => service.Settings).Returns(new ApplicationSettingsModel + { + EnableToastNotifications = false, + }); + this.Localization + .Setup(service => service.GetString(It.IsAny())) + .Returns(this.GetLocalizedString); + } + + public NotificationService CreateService() + { + return new NotificationService( + NullLogger.Instance, + this.Settings.Object, + this.Tray.Object, + this.Localization.Object); + } + + private string GetLocalizedString(string key) + { + return this.localizedStrings.TryGetValue(key, out var value) ? value : key; + } + } + } +} diff --git a/Tests/ThreadPilot.Core.Tests/SettingsViewModelThemeTests.cs b/Tests/ThreadPilot.Core.Tests/SettingsViewModelThemeTests.cs index 499b7cd..f03e1cd 100644 --- a/Tests/ThreadPilot.Core.Tests/SettingsViewModelThemeTests.cs +++ b/Tests/ThreadPilot.Core.Tests/SettingsViewModelThemeTests.cs @@ -1,6 +1,7 @@ namespace ThreadPilot.Core.Tests { using System.Collections.ObjectModel; + using CommunityToolkit.Mvvm.Input; using Microsoft.Extensions.Logging.Abstractions; using Moq; using ThreadPilot.Models; @@ -80,6 +81,45 @@ public async Task ChangingApplyPersistentRulesOnProcessStart_UpdatesSettingAndLo Assert.Contains(entries, entry => entry.Message == "[Settings] Apply saved rules at process start enabled."); } + [Fact] + public async Task ChangingLanguage_AppliesLanguageAndLogsVisibleActivityEntry() + { + var harness = new Harness(); + var viewModel = harness.CreateViewModel(); + + viewModel.Settings.Language = "zh-CN"; + + harness.Localization.Verify(service => service.ApplyLanguage("zh-CN"), Times.Once); + harness.Logging.Verify( + service => service.LogUserActionAsync( + "LanguageChanged", + "Language changed to Simplified Chinese", + null), + Times.Once); + var entry = Assert.Single(await harness.Audit.GetEntriesAsync()); + Assert.Equal("Language changed to Simplified Chinese", entry.Message); + Assert.Equal("Language changed to Simplified Chinese.", viewModel.StatusMessage); + } + + [Fact] + public async Task SaveSettingsCommand_PersistsSelectedLanguage() + { + var harness = new Harness(); + ApplicationSettingsModel? savedSettings = null; + harness.SettingsService + .Setup(service => service.UpdateSettingsAsync(It.IsAny())) + .Callback(settings => savedSettings = (ApplicationSettingsModel)settings.Clone()) + .Returns(Task.CompletedTask); + var viewModel = harness.CreateViewModel(); + viewModel.Settings.Language = "zh-CN"; + + await ((IAsyncRelayCommand)viewModel.SaveSettingsCommand).ExecuteAsync(null); + + Assert.NotNull(savedSettings); + Assert.Equal("zh-CN", savedSettings.Language); + Assert.False(viewModel.HasUnsavedChanges); + } + [Fact] public void SettingsView_ExposesPersistentRuleAutoApplyToggle() { @@ -94,11 +134,30 @@ public void SettingsView_ExposesPersistentRuleAutoApplyToggle() "SettingsView.xaml"); var serialized = File.ReadAllText(settingsViewPath); - Assert.Contains("Text=\"Rules & automation\" Style=\"{StaticResource SectionHeaderStyle}\"", serialized, StringComparison.Ordinal); - Assert.Contains("Text=\"Apply saved rules when matching processes start\"", serialized, StringComparison.Ordinal); + Assert.Contains("Text=\"{DynamicResource SettingsView_RulesAutomation}\" Style=\"{StaticResource SectionHeaderStyle}\"", serialized, StringComparison.Ordinal); + Assert.Contains("Text=\"{DynamicResource SettingsView_ApplyOnStart}\"", serialized, StringComparison.Ordinal); Assert.Contains("TextWrapping=\"Wrap\"", serialized, StringComparison.Ordinal); Assert.Contains("IsChecked=\"{Binding Settings.ApplyPersistentRulesOnProcessStart}\"", serialized, StringComparison.Ordinal); - Assert.Contains("This does not install a Windows Service and does not use registry/IFEO persistence.", serialized, StringComparison.Ordinal); + Assert.Contains("Text=\"{DynamicResource SettingsView_ApplyOnStartDescription}\"", serialized, StringComparison.Ordinal); + } + + [Fact] + public void SettingsView_ExposesOptionalChineseLanguageSelection() + { + var settingsViewPath = Path.Combine( + AppContext.BaseDirectory, + "..", + "..", + "..", + "..", + "..", + "Views", + "SettingsView.xaml"); + var serialized = File.ReadAllText(settingsViewPath); + + Assert.Contains("SelectedValue=\"{Binding Settings.Language}\"", serialized, StringComparison.Ordinal); + Assert.Contains("Tag=\"en-US\"", serialized, StringComparison.Ordinal); + Assert.Contains("Tag=\"zh-CN\"", serialized, StringComparison.Ordinal); } private sealed class Harness @@ -121,6 +180,8 @@ private sealed class Harness public Mock Tray { get; } = new(MockBehavior.Loose); + public Mock Localization { get; } = new(MockBehavior.Loose); + public Mock Logging { get; } = new(MockBehavior.Loose); public ActivityAuditService Audit { get; } = new(NullLogger.Instance); @@ -133,6 +194,9 @@ public Harness(bool initialDarkTheme = false) HasUserThemePreference = initialDarkTheme, }; this.SettingsService.SetupGet(service => service.Settings).Returns(this.settings); + this.Autostart + .Setup(service => service.CheckAutostartStatusAsync()) + .ReturnsAsync(true); this.PowerPlans .Setup(service => service.GetPowerPlansAsync()) .ReturnsAsync(new ObservableCollection()); @@ -159,6 +223,7 @@ public SettingsViewModel CreateViewModel() => this.Theme.Object, this.Tray.Object, new GitHubUpdateChecker(new Mock().Object), + this.Localization.Object, this.Logging.Object, this.Audit); } diff --git a/ViewModels/SettingsViewModel.cs b/ViewModels/SettingsViewModel.cs index 55c81a1..51d2ef3 100644 --- a/ViewModels/SettingsViewModel.cs +++ b/ViewModels/SettingsViewModel.cs @@ -49,6 +49,7 @@ public partial class SettingsViewModel : BaseViewModel private readonly IThemeService themeService; private readonly ISystemTrayService systemTrayService; private readonly GitHubUpdateChecker gitHubUpdateChecker; + private readonly ILocalizationService localizationService; private ApplicationSettingsModel savedSettingsSnapshot; private bool isSyncingFromService = false; private bool? appliedThemePreference; @@ -106,6 +107,7 @@ public SettingsViewModel( IThemeService themeService, ISystemTrayService systemTrayService, GitHubUpdateChecker gitHubUpdateChecker, + ILocalizationService localizationService, IEnhancedLoggingService? enhancedLoggingService = null, IActivityAuditService? activityAuditService = null) : base(logger, enhancedLoggingService, activityAuditService) @@ -119,6 +121,7 @@ public SettingsViewModel( this.themeService = themeService ?? throw new ArgumentNullException(nameof(themeService)); this.systemTrayService = systemTrayService ?? throw new ArgumentNullException(nameof(systemTrayService)); this.gitHubUpdateChecker = gitHubUpdateChecker ?? throw new ArgumentNullException(nameof(gitHubUpdateChecker)); + this.localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService)); // Get version and strip the git commit hash (everything after '+') var rawVersion = typeof(App).Assembly @@ -179,6 +182,13 @@ private void OnSettingsPropertyChanged(object? sender, System.ComponentModel.Pro return; } + if (string.Equals(e.PropertyName, nameof(ApplicationSettingsModel.Language), StringComparison.Ordinal)) + { + this.UpdatePendingChangesState(); + this.ApplyLanguagePreference(this.Settings.Language, logUserAction: true); + return; + } + if (string.Equals(e.PropertyName, nameof(ApplicationSettingsModel.ApplyPersistentRulesOnProcessStart), StringComparison.Ordinal)) { this.UpdatePendingChangesState(); @@ -220,6 +230,31 @@ private void ApplyThemePreference(bool useDarkTheme, bool logUserAction) } } + private void ApplyLanguagePreference(string language, bool logUserAction) + { + var normalizedLanguage = LocalizationService.NormalizeLanguage(language); + try + { + this.localizationService.ApplyLanguage(normalizedLanguage); + this.Settings.Language = normalizedLanguage; + var languageName = normalizedLanguage == LocalizationService.SimplifiedChineseLanguage + ? "Simplified Chinese" + : "English"; + this.StatusMessage = $"Language changed to {languageName}."; + + if (logUserAction) + { + _ = this.LogUserActionAsync("LanguageChanged", $"Language changed to {languageName}"); + } + } + catch (Exception ex) + { + this.StatusMessage = "Failed to change language."; + this.Logger.LogError(ex, "Failed to apply language preference {Language}", normalizedLanguage); + _ = this.LogUserActionAsync("LanguageChangeFailed", $"Failed to change language to {normalizedLanguage}: {ex.Message}"); + } + } + partial void OnHasUnsavedChangesChanged(bool value) { OnPropertyChanged(nameof(CanSaveSettings)); @@ -279,6 +314,7 @@ private async Task SaveSettingsAsync() this.Settings.UseDarkTheme = useDarkTheme; this.isSyncingFromService = false; this.ApplyThemePreference(useDarkTheme, logUserAction: false); + this.ApplyLanguagePreference(this.Settings.Language, logUserAction: false); // Update monitoring services with new settings this.processMonitorManagerService.UpdateSettings(); @@ -549,6 +585,7 @@ public async Task RefreshSettingsAsync() this.Settings.UseDarkTheme = useDarkTheme; this.isSyncingFromService = false; this.ApplyThemePreference(useDarkTheme, logUserAction: false); + this.ApplyLanguagePreference(this.Settings.Language, logUserAction: false); this.SetSavedSettingsSnapshot(this.Settings); this.StatusMessage = "Settings loaded"; @@ -590,6 +627,7 @@ private void OnSettingsServiceSettingsChanged(object? sender, ApplicationSetting this.Settings.DefaultPowerPlanName = this.cachedDefaultPowerPlanName; } this.SetSavedSettingsSnapshot(this.Settings); + this.ApplyLanguagePreference(this.Settings.Language, logUserAction: false); this.StatusMessage = "Settings synchronized"; } finally diff --git a/Views/SettingsView.xaml b/Views/SettingsView.xaml index f2b3ba5..c72bd35 100644 --- a/Views/SettingsView.xaml +++ b/Views/SettingsView.xaml @@ -178,49 +178,60 @@ - + - - + - - - - - + - + + + + + + + + + - + - + - - From edd6808c7dda2bd8eda882a94b38c065ad51eb9e Mon Sep 17 00:00:00 2001 From: Prime Build <162145141+PrimeBuild-pc@users.noreply.github.com> Date: Sat, 6 Jun 2026 18:25:45 +0200 Subject: [PATCH 3/7] Prepare v1.3.0 release (#25) --- Installer/Installer.iss | 2 +- Installer/ThreadPilot.wxs | 2 +- Installer/setup.iss | 2 +- ThreadPilot.csproj | 8 +++--- app.manifest | 2 +- build/build-installer.ps1 | 2 +- build/build-release.ps1 | 2 +- build/package-release-zips.ps1 | 2 +- chocolatey/threadpilot.nuspec | 4 +-- docs/CHANGELOG.md | 22 +++++++++++++++ docs/release/PACKAGING.md | 19 +++++++------ docs/release/RELEASE_NOTES.md | 49 ++++++++++++++-------------------- docs/releases/v1.3.0.md | 48 +++++++++++++++++++++++++++++++++ sonar-project.properties | 2 +- 14 files changed, 113 insertions(+), 53 deletions(-) create mode 100644 docs/releases/v1.3.0.md diff --git a/Installer/Installer.iss b/Installer/Installer.iss index 8beead0..622de70 100644 --- a/Installer/Installer.iss +++ b/Installer/Installer.iss @@ -5,7 +5,7 @@ #define MyAppPublisher "ThreadPilot" #define MyAppURL "https://github.com/" #define MyAppExeName "ThreadPilot.exe" -#define MyAppVersion "1.2.0" +#define MyAppVersion "1.3.0" #ifndef MyWizardStyle #define MyWizardStyle "modern dynamic windows11" diff --git a/Installer/ThreadPilot.wxs b/Installer/ThreadPilot.wxs index c924f7c..b72c9fe 100644 --- a/Installer/ThreadPilot.wxs +++ b/Installer/ThreadPilot.wxs @@ -7,7 +7,7 @@ diff --git a/Installer/setup.iss b/Installer/setup.iss index 9cfc206..25a2870 100644 --- a/Installer/setup.iss +++ b/Installer/setup.iss @@ -11,7 +11,7 @@ #endif #ifndef MyAppVersion - #define MyAppVersion "1.2.0" + #define MyAppVersion "1.3.0" #endif #ifndef MyAppSourceDir diff --git a/ThreadPilot.csproj b/ThreadPilot.csproj index d660017..85e6592 100644 --- a/ThreadPilot.csproj +++ b/ThreadPilot.csproj @@ -16,10 +16,10 @@ link true CS1998;CS0067;CS0414;WFAC010;IL3000;MVVMTK0034 - 1.2.0 - 1.2.0.0 - 1.2.0.0 - 1.2.0 + 1.3.0 + 1.3.0.0 + 1.3.0.0 + 1.3.0 diff --git a/app.manifest b/app.manifest index 970b893..5816daa 100644 --- a/app.manifest +++ b/app.manifest @@ -1,6 +1,6 @@ - + diff --git a/build/build-installer.ps1 b/build/build-installer.ps1 index 5ef8e30..1fbdb8e 100644 --- a/build/build-installer.ps1 +++ b/build/build-installer.ps1 @@ -1,5 +1,5 @@ param( - [string]$Version = "1.2.0", + [string]$Version = "1.3.0", [string]$Configuration = "Release", [switch]$SkipPublish ) diff --git a/build/build-release.ps1 b/build/build-release.ps1 index 681c789..ec4dd78 100644 --- a/build/build-release.ps1 +++ b/build/build-release.ps1 @@ -1,5 +1,5 @@ param( - [string]$Version = "1.2.0", + [string]$Version = "1.3.0", [string]$Configuration = "Release", [string]$Runtime = "win-x64" ) diff --git a/build/package-release-zips.ps1 b/build/package-release-zips.ps1 index 8ff92ec..54c8d59 100644 --- a/build/package-release-zips.ps1 +++ b/build/package-release-zips.ps1 @@ -1,5 +1,5 @@ param( - [string]$Version = "1.2.0" + [string]$Version = "1.3.0" ) $ErrorActionPreference = "Stop" diff --git a/chocolatey/threadpilot.nuspec b/chocolatey/threadpilot.nuspec index 2cbcdc1..e51f187 100644 --- a/chocolatey/threadpilot.nuspec +++ b/chocolatey/threadpilot.nuspec @@ -2,7 +2,7 @@ threadpilot - 1.2.0 + 1.3.0 Codestin Search App Prime Build https://github.com/PrimeBuild-pc/ThreadPilot @@ -15,7 +15,7 @@ false Advanced Windows process and power plan manager with rules automation and performance controls. ThreadPilot process and power plan manager. - https://github.com/PrimeBuild-pc/ThreadPilot/releases/tag/v1.2.0 + https://github.com/PrimeBuild-pc/ThreadPilot/releases/tag/v1.3.0 threadpilot process powerplan performance windows diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index ce8218b..490dda5 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,6 +2,28 @@ All notable changes to this project are documented in this file. +## v1.3.0 - Localization support + +### Added + +- Added localization infrastructure. +- Added English and Simplified Chinese resource dictionaries. +- Added language selector in Settings. +- Added localized notification support for selected user-facing messages. +- Added tests for localization fallback, language persistence, Settings language selection, and localized notifications. + +### Changed + +- English remains the default application language. +- Simplified Chinese is available as an optional display language. +- Project version updated to 1.3.0. + +### Safety + +- Unsupported or invalid language settings now fall back to English. +- Missing translation keys fall back safely to English or the key. +- No changes to elevation, system tweaks, affinity, or privileged operation behavior. + ## v1.2.0 - CPU topology, persistent rules, and process control update ### Added diff --git a/docs/release/PACKAGING.md b/docs/release/PACKAGING.md index 127faf1..5df81e1 100644 --- a/docs/release/PACKAGING.md +++ b/docs/release/PACKAGING.md @@ -178,9 +178,9 @@ Generate manifests locally with: ```powershell ./build/generate-winget-manifests.ps1 ` - -Version "1.2.0" ` - -Tag "v1.2.0" ` - -InstallerUrl "https://github.com/PrimeBuild-pc/ThreadPilot/releases/download/v1.2.0/ThreadPilot_v1.2.0_Setup.exe" ` + -Version "1.3.0" ` + -Tag "v1.3.0" ` + -InstallerUrl "https://github.com/PrimeBuild-pc/ThreadPilot/releases/download/v1.3.0/ThreadPilot_v1.3.0_Setup.exe" ` -InstallerSha256 "" ` -OutputRoot "winget-manifests" ``` @@ -224,9 +224,9 @@ Local packaging-only validation: ```powershell ./build/publish-chocolatey.ps1 ` - -Version "1.2.0" ` - -Tag "v1.2.0" ` - -InstallerPath ".\artifacts\release\installer\ThreadPilot_v1.2.0_Setup.exe" ` + -Version "1.3.0" ` + -Tag "v1.3.0" ` + -InstallerPath ".\artifacts\release\installer\ThreadPilot_v1.3.0_Setup.exe" ` -DryRun ` -PackageOutputDirectory ".\artifacts\choco-dryrun" ` -MetadataOutputPath ".\artifacts\choco-dryrun\chocolatey-package-metadata.json" @@ -250,9 +250,9 @@ Public publish path: ```powershell ./build/publish-chocolatey.ps1 ` - -Version "1.2.0" ` - -Tag "v1.2.0" ` - -InstallerPath ".\artifacts\release\installer\ThreadPilot_v1.2.0_Setup.exe" ` + -Version "1.3.0" ` + -Tag "v1.3.0" ` + -InstallerPath ".\artifacts\release\installer\ThreadPilot_v1.3.0_Setup.exe" ` -ApiKey "" ``` @@ -278,4 +278,3 @@ The release workflow (`.github/workflows/release.yml`) now builds: - ReadyToRun build output for internal CI validation only - SHA-256 checksum manifest for public assets - Optional signing of EXE artifacts when signing secrets are configured - diff --git a/docs/release/RELEASE_NOTES.md b/docs/release/RELEASE_NOTES.md index f434581..193ee1d 100644 --- a/docs/release/RELEASE_NOTES.md +++ b/docs/release/RELEASE_NOTES.md @@ -1,48 +1,39 @@ -# ThreadPilot v1.2.0 Release Notes Draft +# ThreadPilot v1.3.0 Release Notes Draft ## Highlights -- CPU topology v2 with topology-aware `CpuSelection`, CPU Sets, processor groups, and safer handling above 64 logical processors. -- New safe affinity paths where CPU64 no longer aliases CPU0. -- Intel hybrid handling through topology and `EfficiencyClass`, plus AMD CCD/L3-aware preset generation. -- Memory priority support and persistent process rules. -- Apply saved rules automatically when matching processes start while ThreadPilot is running. -- Process tab context menu actions, Save as rule, Apply now, and selected-process summary. +- Added localization infrastructure with English and Simplified Chinese resources. +- English remains the default language. +- Simplified Chinese is available as an optional display language through Settings. +- Selected user-facing notification messages now use localized resources. +- Localization fallback behavior is covered by expanded tests. ## Added -- CPU topology v2 and `CpuSelection` for topology-aware affinity. -- Group-aware CPU Sets support and processor group safety. -- Memory priority controls. -- Persistent process rules with runtime apply-at-process-start support. -- Process tab context menu actions and selected-process summary. -- Optional Diagnostics view hidden by default. +- Added localization infrastructure. +- Added English and Simplified Chinese resource dictionaries. +- Added language selector in Settings. +- Added localized notification support for selected user-facing messages. +- Added tests for localization fallback, language persistence, Settings language selection, and localized notifications. ## Changed -- Default presets are gaming-oriented and generated from topology rather than hardcoded CPU SKU lists. -- Intel hybrid behavior uses Windows topology and `EfficiencyClass`. -- AMD behavior uses CCD/L3-aware preset generation. -- Project version updated to 1.2.0. - -## Fixed - -- Startup no longer fails from a read-only selected-process summary binding. -- CPU64 no longer aliases CPU0 in new safe affinity paths. -- Persistent rule auto-apply cancellation does not log shutdown/future cancellation as a warning. +- English remains the default application language. +- Simplified Chinese is available as an optional display language. +- Project version updated to 1.3.0. ## Safety -- High CPU priority shows a warning and Realtime priority remains blocked. -- ThreadPilot does not bypass anti-cheat or protected-process restrictions. -- Administrator rights may help ordinary access-denied cases but do not bypass protected processes. +- Unsupported or invalid language settings now fall back to English. +- Missing translation keys fall back safely to English or the key. +- No changes to elevation, system tweaks, affinity, or privileged operation behavior. ## Compatibility and Upgrade Notes - Requires Windows 11 build 22000 or newer. -- Existing legacy affinity profiles continue to load. -- New saved rules prefer topology-aware `CpuSelection` when safe topology mapping is available. -- Apply at process start works only while ThreadPilot is running. +- Existing settings continue to load. +- Existing installations without a language setting default to English. +- Existing process, affinity, power plan, and automation behavior is unchanged. ## Known Non-Goals diff --git a/docs/releases/v1.3.0.md b/docs/releases/v1.3.0.md new file mode 100644 index 0000000..193ee1d --- /dev/null +++ b/docs/releases/v1.3.0.md @@ -0,0 +1,48 @@ +# ThreadPilot v1.3.0 Release Notes Draft + +## Highlights + +- Added localization infrastructure with English and Simplified Chinese resources. +- English remains the default language. +- Simplified Chinese is available as an optional display language through Settings. +- Selected user-facing notification messages now use localized resources. +- Localization fallback behavior is covered by expanded tests. + +## Added + +- Added localization infrastructure. +- Added English and Simplified Chinese resource dictionaries. +- Added language selector in Settings. +- Added localized notification support for selected user-facing messages. +- Added tests for localization fallback, language persistence, Settings language selection, and localized notifications. + +## Changed + +- English remains the default application language. +- Simplified Chinese is available as an optional display language. +- Project version updated to 1.3.0. + +## Safety + +- Unsupported or invalid language settings now fall back to English. +- Missing translation keys fall back safely to English or the key. +- No changes to elevation, system tweaks, affinity, or privileged operation behavior. + +## Compatibility and Upgrade Notes + +- Requires Windows 11 build 22000 or newer. +- Existing settings continue to load. +- Existing installations without a language setting default to English. +- Existing process, affinity, power plan, and automation behavior is unchanged. + +## Known Non-Goals + +- No anti-cheat bypass. +- No Windows Service. +- No registry or IFEO persistence. +- No generated release artifacts yet. +- No GitHub release or tag yet. + +## Release Artifact Status + +- Installer, portable ZIP, checksums, package metadata verification, and release upload remain pending manual validation. diff --git a/sonar-project.properties b/sonar-project.properties index c9fb7ea..6425be6 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,6 +1,6 @@ sonar.projectKey=threadpilot sonar.projectName=ThreadPilot -sonar.projectVersion=1.2.0 +sonar.projectVersion=1.3.0 sonar.sourceEncoding=UTF-8 sonar.sources=. From c058fef7d55b050dddbd92d94b0bfb79d39692ff Mon Sep 17 00:00:00 2001 From: Prime Build <162145141+PrimeBuild-pc@users.noreply.github.com> Date: Sun, 7 Jun 2026 04:33:51 +0200 Subject: [PATCH 4/7] Fix v1.3.1 localization and installer cleanup (#26) ## Summary Fixes the incomplete Simplified Chinese localization shipped in v1.3.0 and prepares v1.3.1. ## Changes - Completed zh-CN localization coverage across the main UI. - Added/updated localization keys for primary views, tray/status text, notifications, and settings. - Kept en-US as the default language. - Updated version metadata to 1.3.1. - Changed installer display name to ThreadPilot while keeping version metadata separate. - Added guarded cleanup for obsolete ThreadPilot 0.1.0-beta uninstall entry. - Improved uninstall cleanup for ThreadPilot AppData, startup task, and HKCU Run entry. - Added localization and packaging metadata tests. ## Validation - dotnet build ThreadPilot_1.sln: passed, 0 warnings - dotnet test ThreadPilot_1.sln: passed, 524 tests ## Safety - No change to elevation behavior. - No change to affinity/process control behavior. - No change to power plan or system tweak behavior. - Normal install/update preserves user settings. - Full uninstall removes ThreadPilot-owned AppData for the uninstalling user. --- .github/workflows/release.yml | 17 +- Installer/Installer.iss | 14 +- Installer/ThreadPilot.wxs | 2 +- Installer/setup.iss | 50 +++- Locales/en-US.xaml | 113 ++++++++- Locales/zh-CN.xaml | 111 +++++++++ MainWindow.xaml | 80 +++---- Services/NotificationService.cs | 38 ++++ Services/SystemTrayService.cs | 143 +++++++++--- Services/SystemTrayStatusUpdater.cs | 20 +- .../LocalizationServiceTests.cs | 104 ++++++++- .../MasksViewModelTests.cs | 24 +- .../PackagingMetadataTests.cs | 106 +++++++++ .../PowerPlanViewXamlTests.cs | 6 +- .../ProcessViewXamlBindingTests.cs | 48 ++-- .../SettingsViewModelThemeTests.cs | 19 ++ ThreadPilot.csproj | 8 +- ViewModels/ProcessViewModel.cs | 6 +- ViewModels/SelectedProcessSummaryViewModel.cs | 64 ++++-- ViewModels/SettingsViewModel.cs | 95 ++++---- Views/LogViewerView.xaml | 74 +++--- Views/MasksView.xaml | 40 ++-- Views/PerformanceView.xaml | 94 ++++---- Views/PowerPlanView.xaml | 36 +-- Views/ProcessPowerPlanAssociationView.xaml | 122 +++++----- Views/ProcessView.xaml | 214 +++++++++--------- Views/SettingsView.xaml | 98 ++++---- Views/SettingsWindow.xaml | 14 +- Views/SystemTweaksView.xaml | 12 +- app.manifest | 2 +- build/build-installer.ps1 | 2 +- build/build-release.ps1 | 19 +- build/package-release-zips.ps1 | 19 +- chocolatey/threadpilot.nuspec | 4 +- docs/CHANGELOG.md | 18 ++ docs/release/RELEASE_NOTES.md | 51 ++--- docs/releases/v1.3.1.md | 41 ++++ sonar-project.properties | 2 +- 38 files changed, 1332 insertions(+), 598 deletions(-) create mode 100644 Tests/ThreadPilot.Core.Tests/PackagingMetadataTests.cs create mode 100644 docs/releases/v1.3.1.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2e973aa..d60d726 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -161,18 +161,13 @@ jobs: schtasks /Delete /TN "ThreadPilot_Startup" /F >nul 2>&1 reg delete "HKCU\Software\Microsoft\Windows\CurrentVersion\Run" /v "ThreadPilot" /f >nul 2>&1 - echo [3/4] Optional user data cleanup... - set "REMOVE_DATA=N" - set /p REMOVE_DATA=Do you want to remove user settings at "%APPDATA%\ThreadPilot"? [y/N]: - if /I "%REMOVE_DATA%"=="Y" ( - if exist "%APPDATA%\ThreadPilot" ( - rd /s /q "%APPDATA%\ThreadPilot" - echo User settings removed. - ) else ( - echo No user settings folder found. - ) + echo [3/4] Removing ThreadPilot user data for this Windows account... + rem Full uninstall removes only ThreadPilot-owned per-user AppData. Normal install/update paths never run this script. + if exist "%APPDATA%\ThreadPilot" ( + rd /s /q "%APPDATA%\ThreadPilot" + echo ThreadPilot user data removed. ) else ( - echo User settings were kept. + echo No ThreadPilot user data folder found. ) echo [4/4] Scheduling app folder removal... diff --git a/Installer/Installer.iss b/Installer/Installer.iss index 622de70..9505e78 100644 --- a/Installer/Installer.iss +++ b/Installer/Installer.iss @@ -5,7 +5,7 @@ #define MyAppPublisher "ThreadPilot" #define MyAppURL "https://github.com/" #define MyAppExeName "ThreadPilot.exe" -#define MyAppVersion "1.3.0" +#define MyAppVersion "1.3.1" #ifndef MyWizardStyle #define MyWizardStyle "modern dynamic windows11" @@ -20,7 +20,7 @@ AppId={{A2A4C8B5-4A9A-4B1B-93F4-5F8B1C7E8C2A} AppName={#MyAppName} AppVersion={#MyAppVersion} -AppVerName={#MyAppName} {#MyAppVersion} +AppVerName={#MyAppName} AppPublisher={#MyAppPublisher} AppPublisherURL={#MyAppURL} AppSupportURL={#MyAppURL} @@ -54,7 +54,15 @@ Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: de Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent [UninstallRun] -Filename: taskkill.exe; Parameters: "/IM '{#MyAppExeName}' /F"; Flags: runhidden waituntilterminated; RunOnceId: UninstallKill +Filename: "taskkill.exe"; Parameters: "/IM ""{#MyAppExeName}"" /F"; Flags: runhidden waituntilterminated; RunOnceId: "UninstallKill" +Filename: "schtasks.exe"; Parameters: "/Delete /TN ""ThreadPilot_Startup"" /F"; Flags: runhidden waituntilterminated; RunOnceId: "UninstallRemoveThreadPilotStartupTask" +Filename: "reg.exe"; Parameters: "delete ""HKCU\Software\Microsoft\Windows\CurrentVersion\Run"" /v ""ThreadPilot"" /f"; Flags: runhidden waituntilterminated; RunOnceId: "UninstallRemoveThreadPilotRunEntry" + +; ThreadPilot user data is preserved during install/update and removed only when +; the generated uninstaller runs. Per-user AppData cleanup is limited to the +; account context used by uninstall. +[UninstallDelete] +Type: filesandordirs; Name: "{userappdata}\ThreadPilot" [Code] diff --git a/Installer/ThreadPilot.wxs b/Installer/ThreadPilot.wxs index b72c9fe..3306343 100644 --- a/Installer/ThreadPilot.wxs +++ b/Installer/ThreadPilot.wxs @@ -7,7 +7,7 @@ diff --git a/Installer/setup.iss b/Installer/setup.iss index 25a2870..48135e4 100644 --- a/Installer/setup.iss +++ b/Installer/setup.iss @@ -11,7 +11,7 @@ #endif #ifndef MyAppVersion - #define MyAppVersion "1.3.0" + #define MyAppVersion "1.3.1" #endif #ifndef MyAppSourceDir @@ -22,7 +22,7 @@ AppId={{E8F7A3B2-5C4D-4E6F-8A9B-1C2D3E4F5A6B} AppName={#MyAppName} AppVersion={#MyAppVersion} -AppVerName={#MyAppName} {#MyAppVersion} +AppVerName={#MyAppName} AppPublisher={#MyAppPublisher} AppPublisherURL={#MyAppURL} AppSupportURL={#MyAppURL}/issues @@ -62,3 +62,49 @@ Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon ; Intentionally do not auto-launch after setup to keep package-manager installs unattended. +; ThreadPilot user data is preserved during install/update. Inno removes installed +; files and shortcuts automatically only when the generated uninstaller runs. +; Per-user AppData cleanup is limited to the account context used by uninstall. +[UninstallRun] +Filename: "taskkill.exe"; Parameters: "/IM ""{#MyAppExeName}"" /F"; Flags: runhidden waituntilterminated; RunOnceId: "UninstallKillThreadPilot" +Filename: "schtasks.exe"; Parameters: "/Delete /TN ""ThreadPilot_Startup"" /F"; Flags: runhidden waituntilterminated; RunOnceId: "UninstallRemoveThreadPilotStartupTask" +Filename: "reg.exe"; Parameters: "delete ""HKCU\Software\Microsoft\Windows\CurrentVersion\Run"" /v ""ThreadPilot"" /f"; Flags: runhidden waituntilterminated; RunOnceId: "UninstallRemoveThreadPilotRunEntry" + +[UninstallDelete] +Type: filesandordirs; Name: "{userappdata}\ThreadPilot" + +[Code] +const + LegacyBetaUninstallKey = 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{A2A4C8B5-4A9A-4B1B-93F4-5F8B1C7E8C2A}_is1'; + LegacyBetaDisplayName = 'ThreadPilot 0.1.0-beta'; + +function IsLegacyThreadPilotInstallPath(InstallLocation: string): Boolean; +var + NormalizedLocation: string; + ExpectedInstallRoot: string; +begin + NormalizedLocation := Lowercase(RemoveBackslashUnlessRoot(RemoveQuotes(InstallLocation))); + ExpectedInstallRoot := Lowercase(RemoveBackslashUnlessRoot(ExpandConstant('{autopf}\ThreadPilot'))); + Result := (NormalizedLocation = ExpectedInstallRoot); +end; + +procedure DeleteLegacyBetaUninstallEntry(RootKey: Integer); +var + DisplayName: string; + InstallLocation: string; +begin + if RegQueryStringValue(RootKey, LegacyBetaUninstallKey, 'DisplayName', DisplayName) and + RegQueryStringValue(RootKey, LegacyBetaUninstallKey, 'InstallLocation', InstallLocation) and + (DisplayName = LegacyBetaDisplayName) and + IsLegacyThreadPilotInstallPath(InstallLocation) then + begin + RegDeleteKeyIncludingSubkeys(RootKey, LegacyBetaUninstallKey); + end; +end; + +function InitializeSetup(): Boolean; +begin + DeleteLegacyBetaUninstallEntry(HKLM); + DeleteLegacyBetaUninstallEntry(HKCU); + Result := True; +end; diff --git a/Locales/en-US.xaml b/Locales/en-US.xaml index 41967ab..b09572b 100644 --- a/Locales/en-US.xaml +++ b/Locales/en-US.xaml @@ -30,6 +30,12 @@ Recommended Don't show again Open Settings + Primary navigation + Elevation warning overlay + Performance introduction overlay + Startup minimized suggestion + Unsaved settings dialog + Application startup loading overlay ThreadPilot Activity @@ -94,10 +100,13 @@ Delete selected mask CPUs CPU + Mask Options Optional Diagnostics Diagnostics are optional and intended for troubleshooting. For in-game overlays and detailed performance graphs, use dedicated tools. + Diagnostics are optional and intended for troubleshooting. + For in-game overlays and detailed performance graphs, use dedicated tools. Quick tips: 1. Open diagnostics only when you need a focused troubleshooting snapshot. 2. Review hotspots only as a hint before creating automation rules. @@ -128,6 +137,12 @@ Refresh the current dashboard snapshot Global Power Plan + Process Hotspots + Filter + Memory + Name + Priority + Window Memory Used CPU % Mem % @@ -180,6 +195,7 @@ Rule CPU Mask: Optional: Select a CPU mask to apply when this process starts Rule Priority + Update Optional: Select the process priority to apply when this process starts Association Priority: Higher priority associations take precedence when multiple match @@ -206,6 +222,7 @@ Save Configuration No automation rules yet Status: + Automation Monitoring Start Automation Monitoring Stop Automation Monitoring @@ -226,6 +243,7 @@ Process Management Search, filter, and control active process configurations Search processes by name + Process search Hide Windows system processes Hide System Processes Hide processes with very low CPU usage @@ -276,7 +294,11 @@ Refresh the process list Load More Load more processes - + Refresh processes + Load more processes + Running process list + Virtualized process table with sorting and selection + Process Name Window Title CPU Usage @@ -294,6 +316,32 @@ Apply Pending Settings Apply the pending affinity and selected power plan to the selected process Rules and changes are applied by ThreadPilot only when configured. + Current process status + Advanced affinity picker + Select the pending mask for row context-menu affinity actions + Selected power plan + Save as Rule + No visible window title + Batch + total) + Priority + CPU: unavailable + Memory: unavailable + CPU priority: unavailable + Memory priority unavailable + Affinity: unavailable + No saved rule + No recent ThreadPilot action + Selected process: {0} (PID {1}) + Current process status: protected or access denied + Current process status: selected + CPU: {0:N1}% + Memory: {0} + CPU priority: {0} + Affinity: legacy mask 0x{0:X} + Memory priority: {0} + saved rule + Saved rule exists: {0} Above Normal Below Normal @@ -395,6 +443,46 @@ Import Configuration Save Settings ThreadPilot + Theme changed to {0}. + Failed to change theme to {0}. + Language changed to {0}. + Failed to change language. + Saving settings... + Settings saved with warnings: {0} + Settings saved and applied successfully. + Error saving settings: {0} + Resetting to defaults... + Settings reset to defaults (not saved yet) + Error resetting settings: {0} + Exporting configuration bundle... + Export canceled + Configuration exported to: {0} + Error exporting settings: {0} + Importing configuration... + Import canceled + Configuration bundle imported and applied + Legacy settings imported (rules unchanged) + Error importing settings: {0} + Test notification sent + Error sending test notification: {0} + Loading settings... + Settings loaded + Error loading settings: {0} + Settings synchronized + Checking for updates... + Unable to determine the latest version. + New version available: {0} + Application is up to date. Installed version: {0} + Error while checking updates: {0} + Settings have been modified + Settings match the saved configuration + Simplified Chinese + English + Dark + Light + Export ThreadPilot Configuration + Import ThreadPilot Configuration + Failed to update Windows autostart. Keeping previous autostart state. ThreadPilot Settings @@ -509,7 +597,30 @@ Game Boost mode deactivated after {0} Process Monitor Error Affinity blocked + Affinity applied + Affinity adjusted + Affinity failed + Affinity error Priority blocked + Priority warning + Priority applied + Priority adjusted + Priority error + Keyboard Shortcut + Toggle monitoring shortcut activated + High Performance power plan shortcut activated + Refresh process list shortcut activated + ThreadPilot Started + Process monitoring and power plan management is now active + Startup Error + Failed to start process monitoring manager + Automation Monitoring Error + Settings Saved + Application settings have been saved successfully + Settings Saved with Warnings + Settings Error + Failed to save settings + This is a test notification to verify your settings are working correctly. ThreadPilot requires administrator privileges to manage process affinity and power plans. Would you like to restart the application with administrator privileges? diff --git a/Locales/zh-CN.xaml b/Locales/zh-CN.xaml index 3a9ce20..8f4bcfc 100644 --- a/Locales/zh-CN.xaml +++ b/Locales/zh-CN.xaml @@ -30,6 +30,12 @@ 推荐 不再显示 打开设置 + 主导航 + 权限提升警告覆盖层 + 性能诊断介绍覆盖层 + 启动时最小化建议 + 未保存设置对话框 + 应用启动加载覆盖层 ThreadPilot 活动日志 @@ -94,10 +100,13 @@ 删除选定的掩码 CPU 核心 CPU + 掩码选项 可选性能诊断 诊断是可选的,仅用于故障排除。对于游戏内覆盖层和详细的性能图表,请使用专用工具。 + 诊断是可选的,仅用于故障排除。 + 对于游戏内覆盖层和详细的性能图表,请使用专用工具。 快速提示: 1. 仅在需要针对性的故障排除快照时才打开诊断。 2. 在创建自动化规则之前,仅将热点查看作为提示。 @@ -128,6 +137,12 @@ 刷新当前的仪表板快照 全局电源计划 + 进程热点 + 筛选 + 内存 + 名称 + 优先级 + 窗口 已用内存 CPU % 内存 % @@ -180,6 +195,7 @@ 规则 CPU 掩码: 可选: 选择在该进程启动时要应用的 CPU 掩码 规则优先级 + 更新 可选: 选择在该进程启动时要应用的进程优先级 关联优先级: 当有多个匹配时,优先级较高的关联优先使用 @@ -206,6 +222,7 @@ 保存配置 尚无自动化规则 状态: + 自动化监控 启动自动化监控 停止自动化监控 @@ -226,6 +243,7 @@ 系统进程管理 搜索、过滤和控制活动的进程配置 按名称搜索进程 + 进程搜索 隐藏 Windows 系统进程 隐藏系统进程 隐藏 CPU 使用率极低的进程 @@ -276,6 +294,10 @@ 刷新进程列表 加载更多 加载更多进程 + 刷新进程 + 加载更多进程 + 运行中进程列表 + 支持排序和选择的虚拟化进程表 进程名称 窗口标题 @@ -294,6 +316,32 @@ 应用待处理设置 将待处理的关联性和选定的电源计划应用于选定的进程 ThreadPilot 仅在配置后才应用规则和更改。 + 当前进程状态 + 高级关联性选择器 + 为行上下文菜单关联性操作选择待处理掩码 + 所选电源计划 + 保存为规则 + 无可见窗口标题 + 批次 + 总计) + 优先级 + CPU: 不可用 + 内存: 不可用 + CPU 优先级: 不可用 + 内存优先级不可用 + 关联性: 不可用 + 无已保存规则 + 暂无最近的 ThreadPilot 操作 + 选定进程: {0} (PID {1}) + 当前进程状态: 受保护或访问被拒绝 + 当前进程状态: 已选择 + CPU: {0:N1}% + 内存: {0} + CPU 优先级: {0} + 关联性: 旧版掩码 0x{0:X} + 内存优先级: {0} + 已保存规则 + 存在已保存规则: {0} 高于正常 低于正常 @@ -395,6 +443,46 @@ 导入配置包 保存并应用设置 ThreadPilot + 主题已切换为{0}。 + 无法切换主题为{0}。 + 语言已切换为{0}。 + 无法切换语言。 + 正在保存设置... + 设置已保存但有警告: {0} + 设置已成功保存并应用。 + 保存设置时出错: {0} + 正在重置为默认值... + 设置已重置为默认值(尚未保存) + 重置设置时出错: {0} + 正在导出配置包... + 已取消导出 + 配置已导出到: {0} + 导出设置时出错: {0} + 正在导入配置... + 已取消导入 + 配置包已导入并应用 + 旧版设置已导入(规则未更改) + 导入设置时出错: {0} + 测试通知已发送 + 发送测试通知时出错: {0} + 正在加载设置... + 设置已加载 + 加载设置时出错: {0} + 设置已同步 + 正在检查更新... + 无法确定最新版本。 + 发现新版本: {0} + 应用已是最新版本。已安装版本: {0} + 检查更新时出错: {0} + 设置已被修改 + 设置与已保存的配置一致 + 简体中文 + 英文 + 深色 + 浅色 + 导出 ThreadPilot 配置 + 导入 ThreadPilot 配置 + 无法更新 Windows 自启动。将保留之前的自启动状态。 ThreadPilot 设置 @@ -509,7 +597,30 @@ 游戏加速模式已关闭,持续时间: {0} 进程监控器错误 关联性应用被阻止 + 关联性已应用 + 关联性已调整 + 关联性应用失败 + 关联性错误 优先级应用被阻止 + 优先级警告 + 优先级已应用 + 优先级已调整 + 优先级错误 + 键盘快捷键 + 切换监控快捷键已触发 + 高性能电源计划快捷键已触发 + 刷新进程列表快捷键已触发 + ThreadPilot 已启动 + 进程监控和电源计划管理现已激活 + 启动错误 + 无法启动进程监控管理器 + 自动化监控错误 + 设置已保存 + 应用设置已成功保存 + 设置已保存但有警告 + 设置错误 + 无法保存设置 + 这是一条用于验证您的设置是否正常工作的测试通知。 ThreadPilot 需要管理员权限来管理进程关联性和电源计划。 您想以管理员权限重新启动应用程序吗? diff --git a/MainWindow.xaml b/MainWindow.xaml index 7a203ed..c991768 100644 --- a/MainWindow.xaml +++ b/MainWindow.xaml @@ -10,7 +10,7 @@ mc:Ignorable="d" ExtendsContentIntoTitleBar="True" WindowBackdropType="Mica" - Title="ThreadPilot - Process & Power Plan Manager" + Title="{DynamicResource MainWindow_Title}" Height="864" Width="1280" WindowStartupLocation="CenterScreen" @@ -86,7 +86,7 @@ IsPaneOpen="False" OpenPaneLength="180" CompactPaneLength="52" - AutomationProperties.Name="Primary navigation"> + AutomationProperties.Name="{DynamicResource MainWindow_PrimaryNavigation}"> - - - + @@ -290,22 +290,22 @@ - + - + - + - + - - + - - + - + @@ -168,9 +168,9 @@ - + - - + @@ -219,20 +219,20 @@ - + - @@ -583,7 +583,7 @@ CornerRadius="8"> - @@ -595,7 +595,7 @@ Background="{DynamicResource QuietRowBackgroundBrush}" Margin="0,0,0,12"> - @@ -610,7 +610,7 @@ - - + - + + ToolTip="{DynamicResource ProcessView_AffinityCurrentTooltip}"/> + ToolTip="{DynamicResource ProcessView_AffinityStagedTooltip}"/> - + + ToolTip="{DynamicResource ProcessView_StageMaskTooltip}"> @@ -700,7 +700,7 @@ FontSize="{StaticResource ProcessFontSmall}" Margin="0,2,0,6" Foreground="{Binding IsHyperThreadingActive, Converter={StaticResource BoolToColorConverter}}" - ToolTip="Shows whether Hyper-Threading (Intel) or SMT (AMD) is present and active on this system"/> + ToolTip="{DynamicResource ProcessView_HyperThreadingTooltip}"/> + ToolTip="{DynamicResource ProcessView_CpuSelectionPreviewTooltip}"/> @@ -736,59 +736,59 @@ - + - - - - - - + + + + + +