From 497d0ad9d0ff8d0eeeff3836f46102f1a0340c2a Mon Sep 17 00:00:00 2001 From: Haerbin23456 <60066765+Haerbin23456@users.noreply.github.com> Date: Wed, 4 Mar 2026 13:46:36 +0800 Subject: [PATCH 1/2] feat(i18n): add runtime localization with language switch and dashboard coverage --- ContextMenuProfiler.UI/App.xaml.cs | 1 + .../NullOrEmptyToLocalizedConverter.cs | 32 ++ .../Core/Services/LocalizationService.cs | 283 ++++++++++++++++++ .../Core/Services/UserPreferencesService.cs | 48 +++ ContextMenuProfiler.UI/MainWindow.xaml | 2 +- .../ViewModels/DashboardViewModel.cs | 78 +++-- .../ViewModels/MainWindowViewModel.cs | 74 +++-- .../ViewModels/SettingsViewModel.cs | 26 +- .../Views/Pages/DashboardPage.xaml | 79 ++--- .../Views/Pages/SettingsPage.xaml | 40 ++- 10 files changed, 559 insertions(+), 104 deletions(-) create mode 100644 ContextMenuProfiler.UI/Converters/NullOrEmptyToLocalizedConverter.cs create mode 100644 ContextMenuProfiler.UI/Core/Services/LocalizationService.cs create mode 100644 ContextMenuProfiler.UI/Core/Services/UserPreferencesService.cs diff --git a/ContextMenuProfiler.UI/App.xaml.cs b/ContextMenuProfiler.UI/App.xaml.cs index 08233be..8a7f2d7 100644 --- a/ContextMenuProfiler.UI/App.xaml.cs +++ b/ContextMenuProfiler.UI/App.xaml.cs @@ -21,6 +21,7 @@ public App() protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); + LocalizationService.Instance.InitializeFromPreferences(); LogService.Instance.Info("Application Started"); CleanTempFiles(); CleanHookOutputFiles(); diff --git a/ContextMenuProfiler.UI/Converters/NullOrEmptyToLocalizedConverter.cs b/ContextMenuProfiler.UI/Converters/NullOrEmptyToLocalizedConverter.cs new file mode 100644 index 0000000..1601879 --- /dev/null +++ b/ContextMenuProfiler.UI/Converters/NullOrEmptyToLocalizedConverter.cs @@ -0,0 +1,32 @@ +using ContextMenuProfiler.UI.Core.Services; +using System; +using System.Globalization; +using System.Windows.Data; + +namespace ContextMenuProfiler.UI.Converters +{ + public class NullOrEmptyToLocalizedConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + string text = value?.ToString() ?? string.Empty; + if (!string.IsNullOrWhiteSpace(text)) + { + return text; + } + + string key = parameter?.ToString() ?? string.Empty; + if (string.IsNullOrWhiteSpace(key)) + { + return string.Empty; + } + + return LocalizationService.Instance[key]; + } + + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotImplementedException(); + } + } +} diff --git a/ContextMenuProfiler.UI/Core/Services/LocalizationService.cs b/ContextMenuProfiler.UI/Core/Services/LocalizationService.cs new file mode 100644 index 0000000..4475130 --- /dev/null +++ b/ContextMenuProfiler.UI/Core/Services/LocalizationService.cs @@ -0,0 +1,283 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using System.Collections.ObjectModel; +using System.Globalization; + +namespace ContextMenuProfiler.UI.Core.Services +{ + public class LanguageOption + { + public string Code { get; set; } = ""; + public string DisplayName { get; set; } = ""; + } + + public class LocalizationService : ObservableObject + { + private static LocalizationService? _instance; + public static LocalizationService Instance => _instance ??= new LocalizationService(); + + private readonly Dictionary> _resources = new(StringComparer.OrdinalIgnoreCase); + private string _currentLanguageCode = "en-US"; + + public ReadOnlyCollection AvailableLanguages { get; } + + public string CurrentLanguageCode + { + get => _currentLanguageCode; + private set => SetProperty(ref _currentLanguageCode, value); + } + + public string this[string key] + { + get + { + if (_resources.TryGetValue(CurrentLanguageCode, out var dict) && dict.TryGetValue(key, out var value)) + { + return value; + } + + if (_resources.TryGetValue("en-US", out var fallback) && fallback.TryGetValue(key, out var fallbackValue)) + { + return fallbackValue; + } + + return key; + } + } + + private LocalizationService() + { + AvailableLanguages = new ReadOnlyCollection(new List + { + new LanguageOption { Code = "auto", DisplayName = "System Default" }, + new LanguageOption { Code = "en-US", DisplayName = "English" }, + new LanguageOption { Code = "zh-CN", DisplayName = "简体中文" } + }); + + _resources["en-US"] = BuildEnglish(); + _resources["zh-CN"] = BuildChinese(); + } + + public void InitializeFromPreferences() + { + var prefs = UserPreferencesService.Load(); + ApplyLanguage(prefs.LanguageCode, false); + } + + public void SetLanguage(string code) + { + ApplyLanguage(code, true); + } + + private void ApplyLanguage(string code, bool persist) + { + string resolved = ResolveLanguageCode(code); + CurrentLanguageCode = resolved; + OnPropertyChanged("Item[]"); + + if (persist) + { + UserPreferencesService.Save(new UserPreferences { LanguageCode = code }); + } + } + + private static string ResolveLanguageCode(string? code) + { + if (string.IsNullOrWhiteSpace(code) || code.Equals("auto", StringComparison.OrdinalIgnoreCase)) + { + string system = CultureInfo.CurrentUICulture.Name; + if (system.StartsWith("zh", StringComparison.OrdinalIgnoreCase)) + { + return "zh-CN"; + } + return "en-US"; + } + + if (code.Equals("zh-CN", StringComparison.OrdinalIgnoreCase)) + { + return "zh-CN"; + } + + return "en-US"; + } + + private static Dictionary BuildEnglish() + { + return new Dictionary + { + ["App.Title"] = "Context Menu Profiler", + ["Nav.Dashboard"] = "Dashboard", + ["Nav.Settings"] = "Settings", + ["Tray.Home"] = "Home", + ["Status.Version"] = "Context Menu Profiler v1.0", + ["Hook.NotInjected"] = "Hook: Not Injected", + ["Hook.Inject"] = "Inject Hook", + ["Hook.InjectedIdle"] = "Hook: Injected (Idle)", + ["Hook.Eject"] = "Eject Hook", + ["Hook.Active"] = "Hook: Active", + ["Settings.Title"] = "Settings", + ["Settings.SystemTools"] = "System Tools", + ["Settings.RestartExplorer"] = "Restart Explorer", + ["Settings.RestartExplorerDesc"] = "Restarts Windows Explorer process. Useful when shell extensions are stuck or not loading.", + ["Settings.Restart"] = "Restart", + ["Settings.Language"] = "Language", + ["Settings.LanguageDesc"] = "Change UI language. Applies immediately.", + ["Dialog.ConfirmRestart.Title"] = "Confirm Restart", + ["Dialog.ConfirmRestart.Message"] = "Are you sure you want to restart Windows Explorer?\nThis will temporarily close all folder windows and the taskbar.", + ["Dialog.Error.Title"] = "Error", + ["Dialog.Error.RestartExplorer"] = "Failed to restart Explorer: {0}", + ["Dashboard.ScanSystem"] = "Scan System", + ["Dashboard.AnalyzeFile"] = "Analyze File", + ["Dashboard.Refresh"] = "Refresh (Re-scan)", + ["Dashboard.DeepScan"] = "Deep Scan", + ["Dashboard.DeepScanTip"] = "Scan all file extensions (Slower)", + ["Dashboard.SearchExtensions"] = "Search extensions...", + ["Dashboard.RealWorldLoad"] = "Real-World Load", + ["Dashboard.TotalMenuTime"] = "Total Menu Time", + ["Dashboard.TotalExtensions"] = "Total Extensions", + ["Dashboard.Active"] = "Active", + ["Dashboard.HookWarning"] = "The Hook service is required for accurate load time measurement. If it's disconnected, please try to reconnect.", + ["Dashboard.ReconnectInject"] = "Reconnect / Inject", + ["Dashboard.PerfBreakdown"] = "Performance Breakdown", + ["Dashboard.PerfEstimated"] = "Performance data estimated (Hook unavailable)", + ["Dashboard.Create"] = "Create:", + ["Dashboard.Initialize"] = "Initialize:", + ["Dashboard.Query"] = "Query:", + ["Dashboard.WallClock"] = "Wall Clock:", + ["Dashboard.Total"] = "Total:", + ["Dashboard.Diagnostics"] = "Diagnostics", + ["Dashboard.LockWait"] = "Lock Wait:", + ["Dashboard.PipeConnect"] = "Pipe Connect:", + ["Dashboard.IpcRoundTrip"] = "IPC Round Trip:", + ["Dashboard.Label.Clsid"] = "CLSID:", + ["Dashboard.Label.Binary"] = "Binary:", + ["Dashboard.Label.Details"] = "Details:", + ["Dashboard.Label.Interface"] = "Interface: ", + ["Dashboard.Label.IconSource"] = "Icon Source: ", + ["Dashboard.Label.Threading"] = "Threading: ", + ["Dashboard.Label.RegistryName"] = "Registry Name: ", + ["Dashboard.Label.Registry"] = "Registry:", + ["Dashboard.Label.Package"] = "Package:", + ["Dashboard.Value.Unknown"] = "Unknown", + ["Dashboard.Value.None"] = "None", + ["Dashboard.Action.Copy"] = "Copy", + ["Dashboard.Action.DeletePermanently"] = "Delete Permanently", + ["Dashboard.Sort.LoadDesc"] = "Load Time (High to Low)", + ["Dashboard.Sort.LoadAsc"] = "Load Time (Low to High)", + ["Dashboard.Sort.NameAsc"] = "Name (A-Z)", + ["Dashboard.Sort.Latest"] = "Latest Scanned First", + ["Dashboard.Status.ScanningSystem"] = "Scanning system...", + ["Dashboard.Status.ScanningFile"] = "Scanning: {0}", + ["Dashboard.Status.ScanComplete"] = "Scan complete. Found {0} extensions.", + ["Dashboard.Status.ScanFailed"] = "Scan failed.", + ["Dashboard.Status.Ready"] = "Ready to scan", + ["Dashboard.Status.Unknown"] = "Unknown Status", + ["Dashboard.Notify.ScanComplete.Title"] = "Scan Complete", + ["Dashboard.Notify.ScanComplete.Message"] = "Found {0} extensions.", + ["Dashboard.Notify.ScanCompleteForFile.Message"] = "Found {0} extensions for {1}.", + ["Dashboard.Notify.ScanFailed.Title"] = "Scan Failed", + ["Dashboard.Dialog.SelectFileTitle"] = "Select a file to analyze context menu", + ["Dashboard.Dialog.AllFilesFilter"] = "All files (*.*)|*.*", + ["Dashboard.RealLoad.Measuring"] = "Measuring...", + ["Dashboard.RealLoad.Failed"] = "Failed", + ["Dashboard.RealLoad.Error"] = "Error", + ["Dashboard.Category.All"] = "All", + ["Dashboard.Category.Files"] = "Files", + ["Dashboard.Category.Folders"] = "Folders", + ["Dashboard.Category.Background"] = "Background", + ["Dashboard.Category.Drives"] = "Drives", + ["Dashboard.Category.UwpModern"] = "UWP/Modern", + ["Dashboard.Category.StaticVerbs"] = "Static Verbs" + }; + } + + private static Dictionary BuildChinese() + { + return new Dictionary + { + ["App.Title"] = "右键菜单分析器", + ["Nav.Dashboard"] = "仪表盘", + ["Nav.Settings"] = "设置", + ["Tray.Home"] = "主页", + ["Status.Version"] = "右键菜单分析器 v1.0", + ["Hook.NotInjected"] = "Hook:未注入", + ["Hook.Inject"] = "注入 Hook", + ["Hook.InjectedIdle"] = "Hook:已注入(空闲)", + ["Hook.Eject"] = "卸载 Hook", + ["Hook.Active"] = "Hook:已连接", + ["Settings.Title"] = "设置", + ["Settings.SystemTools"] = "系统工具", + ["Settings.RestartExplorer"] = "重启资源管理器", + ["Settings.RestartExplorerDesc"] = "重启 Windows 资源管理器进程。适用于 Shell 扩展卡住或未加载的情况。", + ["Settings.Restart"] = "重启", + ["Settings.Language"] = "语言", + ["Settings.LanguageDesc"] = "切换界面语言,立即生效。", + ["Dialog.ConfirmRestart.Title"] = "确认重启", + ["Dialog.ConfirmRestart.Message"] = "确定要重启 Windows 资源管理器吗?\n这会暂时关闭所有文件夹窗口和任务栏。", + ["Dialog.Error.Title"] = "错误", + ["Dialog.Error.RestartExplorer"] = "重启资源管理器失败:{0}", + ["Dashboard.ScanSystem"] = "扫描系统", + ["Dashboard.AnalyzeFile"] = "分析文件", + ["Dashboard.Refresh"] = "刷新(重新扫描)", + ["Dashboard.DeepScan"] = "深度扫描", + ["Dashboard.DeepScanTip"] = "扫描全部文件扩展(更慢)", + ["Dashboard.SearchExtensions"] = "搜索扩展...", + ["Dashboard.RealWorldLoad"] = "真实加载", + ["Dashboard.TotalMenuTime"] = "菜单总耗时", + ["Dashboard.TotalExtensions"] = "扩展总数", + ["Dashboard.Active"] = "已启用", + ["Dashboard.HookWarning"] = "准确测量加载时间需要 Hook 服务。如果断开,请尝试重新连接。", + ["Dashboard.ReconnectInject"] = "重连 / 注入", + ["Dashboard.PerfBreakdown"] = "性能明细", + ["Dashboard.PerfEstimated"] = "性能数据为估算值(Hook 不可用)", + ["Dashboard.Create"] = "创建:", + ["Dashboard.Initialize"] = "初始化:", + ["Dashboard.Query"] = "查询:", + ["Dashboard.WallClock"] = "端到端:", + ["Dashboard.Total"] = "合计:", + ["Dashboard.Diagnostics"] = "诊断", + ["Dashboard.LockWait"] = "锁等待:", + ["Dashboard.PipeConnect"] = "管道连接:", + ["Dashboard.IpcRoundTrip"] = "IPC 往返:", + ["Dashboard.Label.Clsid"] = "CLSID:", + ["Dashboard.Label.Binary"] = "二进制:", + ["Dashboard.Label.Details"] = "详情:", + ["Dashboard.Label.Interface"] = "接口:", + ["Dashboard.Label.IconSource"] = "图标来源:", + ["Dashboard.Label.Threading"] = "线程模型:", + ["Dashboard.Label.RegistryName"] = "注册表名称:", + ["Dashboard.Label.Registry"] = "注册表:", + ["Dashboard.Label.Package"] = "包:", + ["Dashboard.Value.Unknown"] = "未知", + ["Dashboard.Value.None"] = "无", + ["Dashboard.Action.Copy"] = "复制", + ["Dashboard.Action.DeletePermanently"] = "永久删除", + ["Dashboard.Sort.LoadDesc"] = "加载时间(高到低)", + ["Dashboard.Sort.LoadAsc"] = "加载时间(低到高)", + ["Dashboard.Sort.NameAsc"] = "名称(A-Z)", + ["Dashboard.Sort.Latest"] = "最近扫描优先", + ["Dashboard.Status.ScanningSystem"] = "正在扫描系统...", + ["Dashboard.Status.ScanningFile"] = "正在扫描:{0}", + ["Dashboard.Status.ScanComplete"] = "扫描完成,共找到 {0} 个扩展。", + ["Dashboard.Status.ScanFailed"] = "扫描失败。", + ["Dashboard.Status.Ready"] = "准备开始扫描", + ["Dashboard.Status.Unknown"] = "未知状态", + ["Dashboard.Notify.ScanComplete.Title"] = "扫描完成", + ["Dashboard.Notify.ScanComplete.Message"] = "共找到 {0} 个扩展。", + ["Dashboard.Notify.ScanCompleteForFile.Message"] = "已为 {1} 找到 {0} 个扩展。", + ["Dashboard.Notify.ScanFailed.Title"] = "扫描失败", + ["Dashboard.Dialog.SelectFileTitle"] = "选择要分析右键菜单的文件", + ["Dashboard.Dialog.AllFilesFilter"] = "所有文件 (*.*)|*.*", + ["Dashboard.RealLoad.Measuring"] = "测量中...", + ["Dashboard.RealLoad.Failed"] = "失败", + ["Dashboard.RealLoad.Error"] = "错误", + ["Dashboard.Category.All"] = "全部", + ["Dashboard.Category.Files"] = "文件", + ["Dashboard.Category.Folders"] = "文件夹", + ["Dashboard.Category.Background"] = "背景", + ["Dashboard.Category.Drives"] = "驱动器", + ["Dashboard.Category.UwpModern"] = "UWP/现代扩展", + ["Dashboard.Category.StaticVerbs"] = "静态命令" + }; + } + } +} diff --git a/ContextMenuProfiler.UI/Core/Services/UserPreferencesService.cs b/ContextMenuProfiler.UI/Core/Services/UserPreferencesService.cs new file mode 100644 index 0000000..045cd77 --- /dev/null +++ b/ContextMenuProfiler.UI/Core/Services/UserPreferencesService.cs @@ -0,0 +1,48 @@ +using System.Text.Json; +using System.IO; + +namespace ContextMenuProfiler.UI.Core.Services +{ + public class UserPreferences + { + public string LanguageCode { get; set; } = "auto"; + } + + public static class UserPreferencesService + { + private static readonly string PreferencesDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ContextMenuProfiler"); + private static readonly string PreferencesPath = Path.Combine(PreferencesDirectory, "preferences.json"); + + public static UserPreferences Load() + { + try + { + if (!File.Exists(PreferencesPath)) + { + return new UserPreferences(); + } + + string json = File.ReadAllText(PreferencesPath); + var prefs = JsonSerializer.Deserialize(json); + return prefs ?? new UserPreferences(); + } + catch + { + return new UserPreferences(); + } + } + + public static void Save(UserPreferences preferences) + { + try + { + Directory.CreateDirectory(PreferencesDirectory); + string json = JsonSerializer.Serialize(preferences, new JsonSerializerOptions { WriteIndented = true }); + File.WriteAllText(PreferencesPath, json); + } + catch + { + } + } + } +} diff --git a/ContextMenuProfiler.UI/MainWindow.xaml b/ContextMenuProfiler.UI/MainWindow.xaml index 0356199..79f6548 100644 --- a/ContextMenuProfiler.UI/MainWindow.xaml +++ b/ContextMenuProfiler.UI/MainWindow.xaml @@ -45,7 +45,7 @@ - + diff --git a/ContextMenuProfiler.UI/ViewModels/DashboardViewModel.cs b/ContextMenuProfiler.UI/ViewModels/DashboardViewModel.cs index 8a1198d..37d812f 100644 --- a/ContextMenuProfiler.UI/ViewModels/DashboardViewModel.cs +++ b/ContextMenuProfiler.UI/ViewModels/DashboardViewModel.cs @@ -144,7 +144,7 @@ partial void OnSelectedSortIndexChanged(int value) } [ObservableProperty] - private string _statusText = "Ready to scan"; + private string _statusText = LocalizationService.Instance["Dashboard.Status.Ready"]; [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(RefreshCommand))] @@ -193,10 +193,10 @@ public string HookStatusMessage { return CurrentHookStatus switch { - HookStatus.Active => "Hook Service Active", - HookStatus.Injected => "DLL Injected, Waiting for Pipe...", - HookStatus.Disconnected => "Hook Disconnected (Injection Required)", - _ => "Unknown Status" + HookStatus.Active => LocalizationService.Instance["Hook.Active"], + HookStatus.Injected => LocalizationService.Instance["Hook.InjectedIdle"], + HookStatus.Disconnected => LocalizationService.Instance["Hook.NotInjected"], + _ => LocalizationService.Instance["Dashboard.Status.Unknown"] }; } } @@ -210,15 +210,17 @@ public DashboardViewModel() // Removed sync ScanResultsView setup // Initialize categories - Categories = new ObservableCollection + ApplyLocalizedCategoryNames(); + LocalizationService.Instance.PropertyChanged += (_, e) => { - new CategoryItem { Name = "All", Tag = "All", Icon = SymbolRegular.TableMultiple20, IsActive = true }, - new CategoryItem { Name = "Files", Tag = "File", Icon = SymbolRegular.Document20 }, - new CategoryItem { Name = "Folders", Tag = "Folder", Icon = SymbolRegular.Folder20 }, - new CategoryItem { Name = "Background", Tag = "Background", Icon = SymbolRegular.Image20 }, - new CategoryItem { Name = "Drives", Tag = "Drive", Icon = SymbolRegular.HardDrive20 }, - new CategoryItem { Name = "UWP/Modern", Tag = "UWP", Icon = SymbolRegular.Box20 }, - new CategoryItem { Name = "Static Verbs", Tag = "Static", Icon = SymbolRegular.PuzzlePiece20 } + if (e.PropertyName == "Item[]") + { + ApplyLocalizedCategoryNames(); + if (!IsBusy) + { + StatusText = LocalizationService.Instance["Dashboard.Status.Ready"]; + } + } }; // Observe Hook status changes to update command availability @@ -307,7 +309,7 @@ private async Task Refresh() private async Task ScanSystem() { _lastScanMode = "System"; - StatusText = "Scanning system..."; + StatusText = LocalizationService.Instance["Dashboard.Status.ScanningSystem"]; IsBusy = true; App.Current.Dispatcher.Invoke(() => @@ -358,14 +360,14 @@ void TryCompleteUiDrain() TryCompleteUiDrain(); await uiDrainTcs.Task; - StatusText = $"Scan complete. Found {Results.Count} extensions."; - NotificationService.Instance.ShowSuccess("Scan Complete", $"Found {Results.Count} extensions."); + StatusText = string.Format(LocalizationService.Instance["Dashboard.Status.ScanComplete"], Results.Count); + NotificationService.Instance.ShowSuccess(LocalizationService.Instance["Dashboard.Notify.ScanComplete.Title"], string.Format(LocalizationService.Instance["Dashboard.Notify.ScanComplete.Message"], Results.Count)); } catch (Exception ex) { LogService.Instance.Error("Scan System Failed", ex); - StatusText = "Scan failed."; - NotificationService.Instance.ShowError("Scan Failed", ex.Message); + StatusText = LocalizationService.Instance["Dashboard.Status.ScanFailed"]; + NotificationService.Instance.ShowError(LocalizationService.Instance["Dashboard.Notify.ScanFailed.Title"], ex.Message); } finally { @@ -403,8 +405,8 @@ private void InsertSorted(BenchmarkResult newItem) private async Task PickAndScanFile() { var dialog = new Microsoft.Win32.OpenFileDialog(); - dialog.Title = "Select a file to analyze context menu"; - dialog.Filter = "All files (*.*)|*.*"; + dialog.Title = LocalizationService.Instance["Dashboard.Dialog.SelectFileTitle"]; + dialog.Filter = LocalizationService.Instance["Dashboard.Dialog.AllFilesFilter"]; if (dialog.ShowDialog() == true) { @@ -419,12 +421,12 @@ private async Task ScanFile(string filePath) _lastScanMode = "File"; _lastScanPath = filePath; - StatusText = $"Scanning: {filePath}"; + StatusText = string.Format(LocalizationService.Instance["Dashboard.Status.ScanningFile"], filePath); IsBusy = true; _scanOrderCounter = 0; Results.Clear(); DisplayResults.Clear(); // Clear display - RealLoadTime = "Measuring..."; + RealLoadTime = LocalizationService.Instance["Dashboard.RealLoad.Measuring"]; try { @@ -456,8 +458,8 @@ private async Task ScanFile(string filePath) InsertSorted(res); } UpdateStats(); - StatusText = $"Scan complete. Found {results.Count} extensions."; - NotificationService.Instance.ShowSuccess("Scan Complete", $"Found {results.Count} extensions for {System.IO.Path.GetFileName(filePath)}."); + StatusText = string.Format(LocalizationService.Instance["Dashboard.Status.ScanComplete"], results.Count); + NotificationService.Instance.ShowSuccess(LocalizationService.Instance["Dashboard.Notify.ScanComplete.Title"], string.Format(LocalizationService.Instance["Dashboard.Notify.ScanCompleteForFile.Message"], results.Count, System.IO.Path.GetFileName(filePath))); } // Run Real-World Benchmark (Parallel but after discovery to avoid COM conflicts if any) @@ -465,10 +467,10 @@ private async Task ScanFile(string filePath) } catch (Exception ex) { - StatusText = "Scan failed."; + StatusText = LocalizationService.Instance["Dashboard.Status.ScanFailed"]; LogService.Instance.Error("File Scan Failed", ex); - NotificationService.Instance.ShowError("Scan Failed", ex.Message); - RealLoadTime = "Error"; + NotificationService.Instance.ShowError(LocalizationService.Instance["Dashboard.Notify.ScanFailed.Title"], ex.Message); + RealLoadTime = LocalizationService.Instance["Dashboard.RealLoad.Error"]; } finally { @@ -487,12 +489,30 @@ private async Task RunRealBenchmark(string? filePath = null) } else { - RealLoadTime = "Failed"; + RealLoadTime = LocalizationService.Instance["Dashboard.RealLoad.Failed"]; } } catch { - RealLoadTime = "Error"; + RealLoadTime = LocalizationService.Instance["Dashboard.RealLoad.Error"]; + } + } + + private void ApplyLocalizedCategoryNames() + { + Categories = new ObservableCollection + { + new CategoryItem { Name = LocalizationService.Instance["Dashboard.Category.All"], Tag = "All", Icon = SymbolRegular.TableMultiple20, IsActive = true }, + new CategoryItem { Name = LocalizationService.Instance["Dashboard.Category.Files"], Tag = "File", Icon = SymbolRegular.Document20 }, + new CategoryItem { Name = LocalizationService.Instance["Dashboard.Category.Folders"], Tag = "Folder", Icon = SymbolRegular.Folder20 }, + new CategoryItem { Name = LocalizationService.Instance["Dashboard.Category.Background"], Tag = "Background", Icon = SymbolRegular.Image20 }, + new CategoryItem { Name = LocalizationService.Instance["Dashboard.Category.Drives"], Tag = "Drive", Icon = SymbolRegular.HardDrive20 }, + new CategoryItem { Name = LocalizationService.Instance["Dashboard.Category.UwpModern"], Tag = "UWP", Icon = SymbolRegular.Box20 }, + new CategoryItem { Name = LocalizationService.Instance["Dashboard.Category.StaticVerbs"], Tag = "Static", Icon = SymbolRegular.PuzzlePiece20 } + }; + if (SelectedCategoryIndex < 0 || SelectedCategoryIndex >= Categories.Count) + { + SelectedCategoryIndex = 0; } } diff --git a/ContextMenuProfiler.UI/ViewModels/MainWindowViewModel.cs b/ContextMenuProfiler.UI/ViewModels/MainWindowViewModel.cs index 07e6ada..cc6db38 100644 --- a/ContextMenuProfiler.UI/ViewModels/MainWindowViewModel.cs +++ b/ContextMenuProfiler.UI/ViewModels/MainWindowViewModel.cs @@ -14,21 +14,31 @@ namespace ContextMenuProfiler.UI.ViewModels public partial class MainWindowViewModel : ObservableObject { [ObservableProperty] - private string _applicationTitle = "Context Menu Profiler"; + private string _applicationTitle = LocalizationService.Instance["App.Title"]; [ObservableProperty] private HookStatus _currentHookStatus = HookStatus.Disconnected; [ObservableProperty] - private string _hookStatusMessage = "Hook: Disconnected"; + private string _hookStatusMessage = LocalizationService.Instance["Hook.NotInjected"]; [ObservableProperty] - private string _hookButtonText = "Inject Hook"; + private string _hookButtonText = LocalizationService.Instance["Hook.Inject"]; private readonly DispatcherTimer _statusTimer; public MainWindowViewModel() { + LocalizationService.Instance.PropertyChanged += (_, e) => + { + if (e.PropertyName == "Item[]") + { + ApplyLocalization(); + _ = UpdateHookStatus(); + } + }; + + ApplyLocalization(); _statusTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(2) }; _statusTimer.Tick += async (s, e) => await UpdateHookStatus(); _statusTimer.Start(); @@ -43,20 +53,45 @@ private async Task UpdateHookStatus() switch (CurrentHookStatus) { case HookStatus.Disconnected: - HookStatusMessage = "Hook: Not Injected"; - HookButtonText = "Inject Hook"; + HookStatusMessage = LocalizationService.Instance["Hook.NotInjected"]; + HookButtonText = LocalizationService.Instance["Hook.Inject"]; break; case HookStatus.Injected: - HookStatusMessage = "Hook: Injected (Idle)"; - HookButtonText = "Eject Hook"; + HookStatusMessage = LocalizationService.Instance["Hook.InjectedIdle"]; + HookButtonText = LocalizationService.Instance["Hook.Eject"]; break; case HookStatus.Active: - HookStatusMessage = "Hook: Active"; - HookButtonText = "Eject Hook"; + HookStatusMessage = LocalizationService.Instance["Hook.Active"]; + HookButtonText = LocalizationService.Instance["Hook.Eject"]; break; } } + private void ApplyLocalization() + { + ApplicationTitle = LocalizationService.Instance["App.Title"]; + MenuItems = new ObservableCollection + { + new NavigationViewItem + { + Content = LocalizationService.Instance["Nav.Dashboard"], + Icon = new SymbolIcon { Symbol = SymbolRegular.Home24 }, + TargetPageType = typeof(DashboardPage) + }, + new NavigationViewItem + { + Content = LocalizationService.Instance["Nav.Settings"], + Icon = new SymbolIcon { Symbol = SymbolRegular.Settings24 }, + TargetPageType = typeof(SettingsPage) + } + }; + + TrayMenuItems = new ObservableCollection + { + new System.Windows.Controls.MenuItem { Header = LocalizationService.Instance["Tray.Home"], Tag = "home" } + }; + } + [RelayCommand] private async Task ToggleHook() { @@ -72,21 +107,7 @@ private async Task ToggleHook() } [ObservableProperty] - private ObservableCollection _menuItems = new() - { - new NavigationViewItem - { - Content = "Dashboard", - Icon = new SymbolIcon { Symbol = SymbolRegular.Home24 }, - TargetPageType = typeof(DashboardPage) - }, - new NavigationViewItem - { - Content = "Settings", - Icon = new SymbolIcon { Symbol = SymbolRegular.Settings24 }, - TargetPageType = typeof(SettingsPage) - } - }; + private ObservableCollection _menuItems = new(); [ObservableProperty] private ObservableCollection _footerMenuItems = new() @@ -94,9 +115,6 @@ private async Task ToggleHook() }; [ObservableProperty] - private ObservableCollection _trayMenuItems = new() - { - new System.Windows.Controls.MenuItem { Header = "Home", Tag = "home" } - }; + private ObservableCollection _trayMenuItems = new(); } } diff --git a/ContextMenuProfiler.UI/ViewModels/SettingsViewModel.cs b/ContextMenuProfiler.UI/ViewModels/SettingsViewModel.cs index c612d69..321f7c7 100644 --- a/ContextMenuProfiler.UI/ViewModels/SettingsViewModel.cs +++ b/ContextMenuProfiler.UI/ViewModels/SettingsViewModel.cs @@ -1,17 +1,39 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; +using ContextMenuProfiler.UI.Core.Services; using System.Diagnostics; using System.Windows; using System; +using System.Collections.ObjectModel; namespace ContextMenuProfiler.UI.ViewModels { public partial class SettingsViewModel : ObservableObject { + [ObservableProperty] + private ObservableCollection _languageOptions = new(); + + [ObservableProperty] + private LanguageOption? _selectedLanguage; + + public SettingsViewModel() + { + LanguageOptions = new ObservableCollection(LocalizationService.Instance.AvailableLanguages); + string savedCode = UserPreferencesService.Load().LanguageCode; + SelectedLanguage = LanguageOptions.FirstOrDefault(l => l.Code.Equals(savedCode, StringComparison.OrdinalIgnoreCase)) + ?? LanguageOptions.FirstOrDefault(l => l.Code == "auto"); + } + + partial void OnSelectedLanguageChanged(LanguageOption? value) + { + if (value == null) return; + LocalizationService.Instance.SetLanguage(value.Code); + } + [RelayCommand] private void RestartExplorer() { - if (MessageBox.Show("Are you sure you want to restart Windows Explorer?\nThis will temporarily close all folder windows and the taskbar.", "Confirm Restart", MessageBoxButton.YesNo, MessageBoxImage.Warning) == MessageBoxResult.Yes) + if (MessageBox.Show(LocalizationService.Instance["Dialog.ConfirmRestart.Message"], LocalizationService.Instance["Dialog.ConfirmRestart.Title"], MessageBoxButton.YesNo, MessageBoxImage.Warning) == MessageBoxResult.Yes) { try { @@ -22,7 +44,7 @@ private void RestartExplorer() } catch (Exception ex) { - MessageBox.Show($"Failed to restart Explorer: {ex.Message}", "Error"); + MessageBox.Show(string.Format(LocalizationService.Instance["Dialog.Error.RestartExplorer"], ex.Message), LocalizationService.Instance["Dialog.Error.Title"]); } } } diff --git a/ContextMenuProfiler.UI/Views/Pages/DashboardPage.xaml b/ContextMenuProfiler.UI/Views/Pages/DashboardPage.xaml index 5c1a420..3687df3 100644 --- a/ContextMenuProfiler.UI/Views/Pages/DashboardPage.xaml +++ b/ContextMenuProfiler.UI/Views/Pages/DashboardPage.xaml @@ -24,6 +24,7 @@ + @@ -68,13 +69,13 @@ - @@ -90,7 +91,7 @@ - + @@ -104,7 +105,7 @@ - + @@ -118,12 +119,12 @@ - + - + @@ -139,12 +140,12 @@ - - + + - + @@ -182,7 +183,7 @@ - + @@ -192,14 +193,14 @@ - - - - - + + + + @@ -366,7 +367,7 @@ - + - +