From cbb5e43a40fd3faf13c13ec431b790612c0f131e Mon Sep 17 00:00:00 2001 From: Zero <1270128439@qq.com> Date: Fri, 22 May 2026 12:01:01 +0800 Subject: [PATCH 1/6] feat(i18n): add language infrastructure with follow-system default Lays groundwork for runtime UI localization. WinAppSDK requires a process restart to apply a new PrimaryLanguageOverride, so the flow is "change in Personalize, restart to see effect". - LanguageHelper: single (Tag?, DisplayName) table. Index 0 is "follow system" (Tag=null -> no override -> WinAppSDK uses the OS locale, then the en-US fallback in csproj). - ApplyPersistedLanguageOverride: JsonDocument-parses settings.json before InitializeComponent, avoiding the impossible read of PrimaryLanguageOverride in unpackaged apps. - App.Restart: AppInstance.Restart + manual fallback after cleanup, necessary because xray.exe is not in our job object. - Personalize ComboBox: data-driven (ItemsSource + DataTemplate); adding a language is a one-line edit in LanguageHelper. - Restart InfoBar tracks divergence from the loaded value -- flipping back to the initial choice clears it. - Done command persists Language too (parity with Theme / Backdrop). UI text remains hardcoded Chinese -- extraction lands in a later commit. Co-Authored-By: Claude Opus 4.7 --- App.xaml.cs | 67 +++++++++++++++++++ Helpers/AppPaths.cs | 2 + Helpers/L.cs | 14 ++++ Helpers/LanguageHelper.cs | 100 +++++++++++++++++++++++++++++ Helpers/Loc.cs | 20 ++++++ Models/AppSettings.cs | 4 ++ Services/SettingsService.cs | 2 +- Strings/en-US/Resources.resw | 65 +++++++++++++++++++ Strings/zh-CN/Resources.resw | 65 +++++++++++++++++++ ViewModels/MainViewModel.cs | 1 + ViewModels/PersonalizeViewModel.cs | 53 +++++++++++++++ Views/PersonalizeControl.xaml | 59 +++++++++++++++++ Views/PersonalizeControl.xaml.cs | 6 ++ XrayUI-dev.csproj | 4 ++ 14 files changed, 461 insertions(+), 1 deletion(-) create mode 100644 Helpers/L.cs create mode 100644 Helpers/LanguageHelper.cs create mode 100644 Helpers/Loc.cs create mode 100644 Strings/en-US/Resources.resw create mode 100644 Strings/zh-CN/Resources.resw diff --git a/App.xaml.cs b/App.xaml.cs index d43e459..5bf1ca4 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -1,8 +1,12 @@ using System; using System.Diagnostics; +using System.IO; using System.Runtime.InteropServices; +using System.Text.Json; using System.Threading.Tasks; +using Microsoft.UI.Xaml; using Microsoft.Windows.AppLifecycle; +using XrayUI.Helpers; using XrayUI.Services; namespace XrayUI @@ -23,6 +27,10 @@ public partial class App public App() { + // Must run before InitializeComponent — the XAML resource loader caches the + // current locale at first touch, and that happens during component init. + ApplyPersistedLanguageOverride(); + this.InitializeComponent(); ConfigureProcessShutdownBehavior(); @@ -30,6 +38,26 @@ public App() AppDomain.CurrentDomain.ProcessExit += (_, _) => CleanupOnExit(); } + private static void ApplyPersistedLanguageOverride() + { + try + { + if (!File.Exists(AppPaths.SettingsJsonPath)) return; + + using var stream = File.OpenRead(AppPaths.SettingsJsonPath); + using var doc = JsonDocument.Parse(stream); + if (doc.RootElement.TryGetProperty("Language", out var langElem) + && langElem.ValueKind == JsonValueKind.String) + { + LanguageHelper.ApplyOverride(langElem.GetString()); + } + } + catch (Exception ex) + { + Debug.WriteLine($"[Language] Failed to load persisted language: {ex.Message}"); + } + } + protected override async void OnLaunched(LaunchActivatedEventArgs args) { var cmdArgs = Environment.GetCommandLineArgs(); @@ -89,6 +117,45 @@ public void RequestShutdown(bool fastShutdown = false) Environment.Exit(0); } + /// + /// Restart the process. xray.exe is a child process not bound to a job object, so + /// bare termination would leak it (and the system proxy) — and + /// itself bypasses Window.Closed / ProcessExit, so cleanup must be triggered explicitly here. + /// + public static void Restart() + { + (Application.Current as App)?.CleanupOnExit(fastShutdown: true); + + // Restart terminates the process on success; any synchronous return (or + // exception) means the platform refused — fall through to a manual relaunch. + try { AppInstance.Restart(string.Empty); } catch { } + + var exePath = Process.GetCurrentProcess().MainModule?.FileName; + if (!string.IsNullOrEmpty(exePath)) + { + // Let an external process wait for this one to exit before relaunching. + // An in-process delayed task would be killed by Environment.Exit below. + try + { + Process.Start(new ProcessStartInfo + { + FileName = Environment.GetEnvironmentVariable("ComSpec") ?? "cmd.exe", + Arguments = $"/c ping 127.0.0.1 -n 2 > nul & start \"\" \"{exePath}\"", + WorkingDirectory = AppContext.BaseDirectory, + UseShellExecute = false, + CreateNoWindow = true, + WindowStyle = ProcessWindowStyle.Hidden, + }); + } + catch + { + // Worst case the user relaunches manually. + } + } + + Environment.Exit(0); + } + public void HandleSessionEnding() { CleanupOnExit(fastShutdown: true); diff --git a/Helpers/AppPaths.cs b/Helpers/AppPaths.cs index 88f79bf..418693e 100644 --- a/Helpers/AppPaths.cs +++ b/Helpers/AppPaths.cs @@ -10,5 +10,7 @@ public static class AppPaths "XrayUI"); public static string UpdatesDir { get; } = Path.Combine(LocalAppDataDir, "Updates"); + + public static string SettingsJsonPath { get; } = Path.Combine(LocalAppDataDir, "settings.json"); } } diff --git a/Helpers/L.cs b/Helpers/L.cs new file mode 100644 index 0000000..bfbced9 --- /dev/null +++ b/Helpers/L.cs @@ -0,0 +1,14 @@ +namespace XrayUI.Helpers; + +/// +/// Strongly-typed accessors for resource strings. Each property is the canonical +/// way to look up a localized string from C# — compiler catches typos, IDE +/// supports go-to-definition. XAML still uses x:Uid on the same key +/// (the resw entry key matches the property name 1:1). +/// +/// Entries are added incrementally as call sites are localized. Empty for now — +/// see commit 3 of the i18n series. +/// +public static class L +{ +} diff --git a/Helpers/LanguageHelper.cs b/Helpers/LanguageHelper.cs new file mode 100644 index 0000000..537ce6d --- /dev/null +++ b/Helpers/LanguageHelper.cs @@ -0,0 +1,100 @@ +using System; +using System.Diagnostics; +using Microsoft.Windows.Globalization; + +namespace XrayUI.Helpers +{ + /// + /// One row in . Tag = null + /// means "follow system" (no PrimaryLanguageOverride applied). + /// Top-level (not nested) so XAML's x:DataType can reference it directly. + /// + public sealed partial class LanguageInfo + { + public string? Tag { get; } + public string DisplayName { get; } + public LanguageInfo(string? tag, string displayName) + { + Tag = tag; + DisplayName = displayName; + } + } + + /// + /// Drives the WinAppSDK . + /// Adding a new language means adding one row to — + /// the UI dropdown, persisted-setting normalization and index lookups all read + /// from this single table. + /// + /// Index 0 is the "follow system" choice (Tag = null): when applied, no override + /// is set and WinAppSDK falls back to the OS locale. + /// + public static class LanguageHelper + { + public static readonly LanguageInfo[] SupportedLanguages = + [ + new(null, "跟随系统"), // index 0 — leaves PrimaryLanguageOverride alone + new("zh-CN", "简体中文"), + new("en-US", "English"), + ]; + + /// + /// Returns the canonical tag if supported. A null return is also the + /// signal to skip (i.e. follow system) — that + /// mapping is deliberate, not an error case. + /// + public static string? Normalize(string? tag) + { + if (string.IsNullOrEmpty(tag)) return null; + foreach (var lang in SupportedLanguages) + { + if (lang.Tag is not null + && string.Equals(lang.Tag, tag, StringComparison.OrdinalIgnoreCase)) + return lang.Tag; + } + return null; + } + + /// + /// 0-based index in . null and unknown + /// tags both map to index 0 ("follow system"), keeping the UI dropdown + /// truthful when no explicit choice has been persisted. + /// + public static int IndexOf(string? tag) + { + if (string.IsNullOrEmpty(tag)) return 0; + for (int i = 0; i < SupportedLanguages.Length; i++) + { + if (string.Equals(SupportedLanguages[i].Tag, tag, StringComparison.OrdinalIgnoreCase)) + return i; + } + return 0; + } + + /// Tag at the given index. Index 0 ("follow system") returns null. + public static string? TagAt(int index) + => (uint)index < (uint)SupportedLanguages.Length + ? SupportedLanguages[index].Tag + : null; + + /// + /// Sets (or, when is null / unsupported, leaves + /// alone) the WinAppSDK language override. Must be called before any XAML + /// resource resolution — i.e. before App.InitializeComponent. + /// + public static void ApplyOverride(string? tag) + { + var language = Normalize(tag); + if (string.IsNullOrEmpty(language)) return; + + try + { + ApplicationLanguages.PrimaryLanguageOverride = language; + } + catch (Exception ex) + { + Debug.WriteLine($"[Language] Failed to apply '{language}': {ex.Message}"); + } + } + } +} diff --git a/Helpers/Loc.cs b/Helpers/Loc.cs new file mode 100644 index 0000000..6d126ec --- /dev/null +++ b/Helpers/Loc.cs @@ -0,0 +1,20 @@ +using Microsoft.Windows.ApplicationModel.Resources; + +namespace XrayUI.Helpers; + +/// +/// Thin wrapper over WinAppSDK's . The default +/// constructor resolves to the "Resources" resource map (which maps to +/// Strings/{lang}/Resources.resw). Cached once at static init; values +/// are frozen at the locale active when this class is first touched, so any +/// language change requires a process restart. +/// +public static class Loc +{ + private static readonly ResourceLoader _loader = new(); + + public static string GetString(string key) => _loader.GetString(key); + + public static string Format(string key, params object?[] args) => + string.Format(_loader.GetString(key), args); +} diff --git a/Models/AppSettings.cs b/Models/AppSettings.cs index b90a78b..a702e51 100644 --- a/Models/AppSettings.cs +++ b/Models/AppSettings.cs @@ -25,6 +25,10 @@ public class AppSettings /// "" | "quarter" | "half" | "full"; controls Xray log IP masking. public string LogMaskAddress { get; set; } = string.Empty; + // ── Internationalization ────────────────────────────────────────────── + /// BCP-47 tag from , or null to follow system. + public string? Language { get; set; } + // ── Personalization ─────────────────────────────────────────────────── /// "Light" | "Dark" | "Default" (follows system) public string? ThemeSetting { get; set; } diff --git a/Services/SettingsService.cs b/Services/SettingsService.cs index 8815d65..1a2c776 100644 --- a/Services/SettingsService.cs +++ b/Services/SettingsService.cs @@ -14,7 +14,7 @@ public class SettingsService { private static readonly string DataDir = AppPaths.LocalAppDataDir; - private static readonly string SettingsFile = Path.Combine(DataDir, "settings.json"); + private static readonly string SettingsFile = AppPaths.SettingsJsonPath; private static readonly string ServersFile = Path.Combine(DataDir, "servers.json"); private AppSettings? _cachedSettings; diff --git a/Strings/en-US/Resources.resw b/Strings/en-US/Resources.resw new file mode 100644 index 0000000..1feca4c --- /dev/null +++ b/Strings/en-US/Resources.resw @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + en-US + Sentinel — confirms the en-US resw is selected. Real keys land in commit 3. + + diff --git a/Strings/zh-CN/Resources.resw b/Strings/zh-CN/Resources.resw new file mode 100644 index 0000000..e7fa934 --- /dev/null +++ b/Strings/zh-CN/Resources.resw @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + zh-CN + Sentinel — confirms the zh-CN resw is selected. Real keys land in commit 3. + + diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs index 6129ac4..bfddeba 100644 --- a/ViewModels/MainViewModel.cs +++ b/ViewModels/MainViewModel.cs @@ -119,6 +119,7 @@ public async Task InitializeAsync(bool isBootLaunch = false) ControlPanel.IsSystemProxyEnabled = s.IsSystemProxyEnabled; ControlPanel.InitializePersonalize(s); Personalize.LoadDisplayOptions(s); + Personalize.LoadLanguage(s); ServerDetail.ShowLatencyInDetails = s.ShowLatencyInDetails; ServerDetail.ShowAiUnlockInDetails = s.ShowAiUnlockInDetails; diff --git a/ViewModels/PersonalizeViewModel.cs b/ViewModels/PersonalizeViewModel.cs index d7364d4..e92feb7 100644 --- a/ViewModels/PersonalizeViewModel.cs +++ b/ViewModels/PersonalizeViewModel.cs @@ -20,6 +20,9 @@ public partial class PersonalizeViewModel : ObservableObject private int _selectedThemeIndex; private int _selectedBackdropIndex; + private int _selectedLanguageIndex; + private int _initialLanguageIndex = -1; + private bool _showLanguageRestartHint; private bool _showLatencyInDetails = true; private bool _showAiUnlockInDetails = true; @@ -131,6 +134,42 @@ public int SelectedBackdropIndex } } + // ── Language ────────────────────────────────────────────────────────── + + /// Bound to the language ComboBox's ItemsSource — single source of truth + /// for the dropdown contents. Adding a language is a one-line edit in LanguageHelper. + public LanguageInfo[] SupportedLanguages => LanguageHelper.SupportedLanguages; + + public int SelectedLanguageIndex + { + get => _selectedLanguageIndex; + set + { + if (!SetProperty(ref _selectedLanguageIndex, value)) return; + // Hint visibility tracks divergence from the loaded value, not "has the + // user touched the dropdown" — flipping back to the initial choice means + // no restart is needed, so the hint should disappear too. The -1 guard + // suppresses the side effect during the initial LoadLanguage call. + if (_initialLanguageIndex >= 0) + ShowLanguageRestartHint = value != _initialLanguageIndex; + } + } + + public bool ShowLanguageRestartHint + { + get => _showLanguageRestartHint; + set => SetProperty(ref _showLanguageRestartHint, value); + } + + /// Persist the currently-selected language. Call right before . + public async Task ApplyLanguageAsync() + { + var tag = LanguageHelper.TagAt(_selectedLanguageIndex); + var s = await _settings.LoadSettingsAsync(); + s.Language = tag; + await _settings.SaveSettingsAsync(s); + } + public bool ShowLatencyInDetails { get => _showLatencyInDetails; @@ -190,6 +229,10 @@ private async Task Done() s.BackdropSetting = ThemeHelper.CurrentBackdrop; s.ShowLatencyInDetails = ShowLatencyInDetails; s.ShowAiUnlockInDetails = ShowAiUnlockInDetails; + // Language doesn't take effect until the next process start, but Done still + // persists it — otherwise the user would have to click the restart hint to + // save at all, which is surprising compared to how Theme / Backdrop behave. + s.Language = LanguageHelper.TagAt(_selectedLanguageIndex); await _settings.SaveSettingsAsync(s); CloseRequested?.Invoke(this, EventArgs.Empty); } @@ -227,5 +270,15 @@ public void LoadDisplayOptions(AppSettings settings) ShowLatencyInDetails = settings.ShowLatencyInDetails; ShowAiUnlockInDetails = settings.ShowAiUnlockInDetails; } + + public void LoadLanguage(AppSettings settings) + { + // Assign through the field to bypass the setter's InfoBar side effect, then + // record this as the baseline so divergence-from-baseline drives the hint. + var index = LanguageHelper.IndexOf(settings.Language); + _selectedLanguageIndex = index; + _initialLanguageIndex = index; + OnPropertyChanged(nameof(SelectedLanguageIndex)); + } } } diff --git a/Views/PersonalizeControl.xaml b/Views/PersonalizeControl.xaml index 00c94d1..0f6deff 100644 --- a/Views/PersonalizeControl.xaml +++ b/Views/PersonalizeControl.xaml @@ -3,6 +3,7 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="using:CommunityToolkit.WinUI.Controls" + xmlns:helpers="using:XrayUI.Helpers" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" @@ -71,6 +72,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -51,7 +51,8 @@ - + Background="Transparent"> @@ -151,8 +151,7 @@ Width="28" Height="22" Padding="0" BorderThickness="0" - Background="Transparent" - ToolTipService.ToolTip="关闭"> + Background="Transparent"> diff --git a/MainWindow.xaml.cs b/MainWindow.xaml.cs index 43d5f53..210331d 100644 --- a/MainWindow.xaml.cs +++ b/MainWindow.xaml.cs @@ -61,6 +61,11 @@ public MainWindow(bool startMinimized = false) InitializeComponent(); + Title = L.MainWindow_Title; + ToolTipService.SetToolTip(DockButton, L.MainWindow_ToggleMini); + ToolTipService.SetToolTip(MiniExpandButton, L.MainWindow_ExpandFull); + ToolTipService.SetToolTip(MinicloseButton, L.MainWindow_Close); + var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this); var scale = DpiHelper.GetWindowScale(hWnd); AppWindow.Resize(new SizeInt32((int)Math.Round(950 * scale), (int)Math.Round(600 * scale))); @@ -146,13 +151,13 @@ private void ConfigureTray() { var flyout = new MenuFlyout(); - var openItem = new MenuFlyoutItem { Text = "\u6253\u5f00\u7a97\u53e3" }; + var openItem = new MenuFlyoutItem { Text = L.Tray_Open }; openItem.Click += (_, _) => RestoreFromTray(); flyout.Items.Add(openItem); flyout.Items.Add(new MenuFlyoutSeparator()); - var exitItem = new MenuFlyoutItem { Text = "\u9000\u51fa" }; + var exitItem = new MenuFlyoutItem { Text = L.Tray_Exit }; exitItem.Click += (_, _) => ExitApplication(); flyout.Items.Add(exitItem); diff --git a/Models/SubscriptionEntry.cs b/Models/SubscriptionEntry.cs index 450774a..714d0fe 100644 --- a/Models/SubscriptionEntry.cs +++ b/Models/SubscriptionEntry.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using System.Runtime.CompilerServices; using System.Text.Json.Serialization; +using XrayUI.Helpers; namespace XrayUI.Models { @@ -60,15 +61,15 @@ public string LastUpdatedText { get { - if (!_lastUpdated.HasValue) return "上次更新: 从未更新"; + if (!_lastUpdated.HasValue) return L.Subscription_NeverUpdated; var delta = DateTimeOffset.Now - _lastUpdated.Value; string rel; - if (delta.TotalSeconds < 60) rel = "刚刚"; - else if (delta.TotalMinutes < 60) rel = $"{(int)delta.TotalMinutes} 分钟前"; - else if (delta.TotalHours < 24) rel = $"{(int)delta.TotalHours} 小时前"; - else if (delta.TotalDays < 30) rel = $"{(int)delta.TotalDays} 天前"; + if (delta.TotalSeconds < 60) rel = L.Subscription_JustNow; + else if (delta.TotalMinutes < 60) rel = Loc.Format("Subscription_MinutesAgo", (int)delta.TotalMinutes); + else if (delta.TotalHours < 24) rel = Loc.Format("Subscription_HoursAgo", (int)delta.TotalHours); + else if (delta.TotalDays < 30) rel = Loc.Format("Subscription_DaysAgo", (int)delta.TotalDays); else rel = _lastUpdated.Value.LocalDateTime.ToString("yyyy-MM-dd"); - return $"上次更新: {rel}"; + return Loc.Format("Subscription_LastUpdated", rel); } } diff --git a/Services/UpdateService.cs b/Services/UpdateService.cs index 4246354..8760b0a 100644 --- a/Services/UpdateService.cs +++ b/Services/UpdateService.cs @@ -104,19 +104,19 @@ public async Task DownloadVerifyAndExtractAsync( using var client = CreateHttpClient(proxyUrl, TimeSpan.FromMinutes(10)); // ── 1. .sha256 first (small, fail-fast on bad release) ───────────────── - progress.Report(new ProgressDialogUpdate("正在获取校验文件…")); + progress.Report(new ProgressDialogUpdate(Loc.GetString("Update_FetchingChecksum"))); string expectedHash; try { var shaText = await client.GetStringAsync(info.Sha256Url, ct); expectedHash = ParseSha256SumLine(shaText) - ?? throw new InvalidDataException("更新校验文件格式异常"); + ?? throw new InvalidDataException(Loc.GetString("Update_ChecksumFormatError")); } catch (OperationCanceledException) { throw; } catch (InvalidDataException) { throw; } catch (Exception ex) { - throw new InvalidDataException("无法下载更新校验文件:" + ex.Message); + throw new InvalidDataException(Loc.GetString("Update_ChecksumDownloadFailed") + ex.Message); } // ── 2. zip — hash is computed during the streaming write so we don't ── @@ -126,27 +126,27 @@ public async Task DownloadVerifyAndExtractAsync( client, info.ZipUrl, zipPath, info.ZipAssetName, progress, ct); if (!string.Equals(actualHash, expectedHash, StringComparison.OrdinalIgnoreCase)) - throw new InvalidDataException("更新包校验失败:SHA256 与服务器公布的不一致。"); + throw new InvalidDataException(Loc.GetString("Update_ChecksumMismatch")); // ── 4. Extract ────────────────────────────────────────────────────────── - progress.Report(new ProgressDialogUpdate("正在解压更新包…")); + progress.Report(new ProgressDialogUpdate(Loc.GetString("Update_Extracting"))); try { await ZipFile.ExtractToDirectoryAsync(zipPath, extractDir, overwriteFiles: true, cancellationToken: ct); } catch (Exception ex) { - throw new InvalidDataException("更新包解压失败:" + ex.Message); + throw new InvalidDataException(Loc.GetString("Update_ExtractFailed") + ex.Message); } // ── 5. Sanity check extracted contents ────────────────────────────────── - progress.Report(new ProgressDialogUpdate("正在验证更新包…")); + progress.Report(new ProgressDialogUpdate(Loc.GetString("Update_Verifying"))); var newAppExe = Path.Combine(extractDir, AppExeName); var newUpdaterExe = Path.Combine(extractDir, UpdaterExeName); if (!File.Exists(newAppExe) || !File.Exists(newUpdaterExe)) - throw new InvalidDataException("更新包内容异常:缺少必要文件。"); + throw new InvalidDataException(Loc.GetString("Update_MissingFiles")); var actualFileVersion = FileVersionInfo.GetVersionInfo(newAppExe).FileVersion; if (string.IsNullOrEmpty(actualFileVersion) || @@ -154,7 +154,7 @@ public async Task DownloadVerifyAndExtractAsync( NormalizeForCompare(parsedFv) != NormalizeForCompare(info.NewVersion)) { throw new InvalidDataException( - $"更新包版本不匹配:期望 {info.NewVersion},实际 {actualFileVersion}。"); + Loc.Format("Update_VersionMismatch", info.NewVersion, actualFileVersion)); } // ── 6. Stage a runnable copy of the CURRENT updater ───────────────────── @@ -167,14 +167,14 @@ public async Task DownloadVerifyAndExtractAsync( if (!File.Exists(currentUpdater)) { throw new FileNotFoundException( - "缺少升级器组件,请重新下载完整安装包。", + Loc.GetString("Update_MissingUpdater"), currentUpdater); } var stagedRunner = Path.Combine(runnerDir, UpdaterExeName); File.Copy(currentUpdater, stagedRunner, overwrite: true); - progress.Report(new ProgressDialogUpdate("正在准备重启…")); + progress.Report(new ProgressDialogUpdate(Loc.GetString("Update_PrepRestart"))); return new UpdateStaging(extractDir, stagedRunner, installDir, info.NewVersion); } @@ -310,11 +310,11 @@ private static ProgressDialogUpdate FormatProgress(string name, long received, l var mbTotal = total.Value / 1024.0 / 1024.0; var percent = received * 100.0 / total.Value; return new ProgressDialogUpdate( - $"正在下载 {name} … {mbReceived:0.0} / {mbTotal:0.0} MB", + Loc.Format("Update_Downloading", name, mbReceived, mbTotal), percent); } - return new ProgressDialogUpdate($"正在下载 {name} … {mbReceived:0.0} MB"); + return new ProgressDialogUpdate(Loc.Format("Update_DownloadingNoTotal", name, mbReceived)); } } } diff --git a/Strings/en-US/Resources.resw b/Strings/en-US/Resources.resw index 790f56c..0d2e4c3 100644 --- a/Strings/en-US/Resources.resw +++ b/Strings/en-US/Resources.resw @@ -118,7 +118,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - Proxy Console + Proxy Panel Toggle mini window @@ -130,17 +130,11 @@ Close - Open Window + Open Exit - - Server List - - - Add - Edit @@ -150,36 +144,6 @@ Share - - Import Nodes - - - Add Subscription - - - Add Manually - - - Search servers - - - Filter: - - - Filter - - - Sort - - - Default - - - Active - - - Protocol - Only available when filter is "All Servers" @@ -195,9 +159,6 @@ (Deleted Subscription) - - ● Active - Start @@ -213,81 +174,30 @@ Personalize - - GitHub - - - Edit Local Port - - - Log - - - Proxy Setting - - - Global Proxy - - - No Proxy Takeover - - - Startup - - - Routing Settings - - Global + Global - Smart - - - Custom Rules... - - - TUN Mode - - - Routing: + Smart New version {0} - - Server Details - - - Name - - - Protocol - Address Port - - Transport - Encryption Security - - AI Access Unlock - Open {0} in browser - - Latency - Retest latency @@ -315,9 +225,6 @@ Theme - - Choose the app appearance mode - Light @@ -330,9 +237,6 @@ Background Effect - - Choose window background material - Acrylic @@ -351,35 +255,15 @@ Data Management - - Export Configuration - - - Export - Done Export Successful - - Exported to the Import folder. - - - Language - - - Choose the app display language - - - Changing language requires a restart - - - Restart Now - - - Later + + Follow System + Display name of the "follow system" language row — referenced by LanguageInfo.DisplayName when Tag is null. Add Subscription @@ -387,33 +271,12 @@ Manage Subscriptions - - Subscription URL - - - Name (optional) - - - Leave empty to use link domain - - - All nodes from the subscription will be imported automatically - - - No subscriptions - Refresh Subscription Delete Subscription - - Delete subscription? - - - All nodes under this subscription will also be deleted. - Subscription fetch failed @@ -432,60 +295,12 @@ Custom Routing Rules - - Rules - Update GeoFile routing data - - Add - - - Save - - - Cancel - - - Edit - - - Delete - - - Global routing is active — custom rules are not in effect. They will apply when switching back to Smart Routing. - Add Rule - - Add - - - Type - - - Domain - - - Match Content - - - Supports exact match, regexp:, geosite:, geoip: prefixes - - - Outbound - - - proxy - - - direct - - - block - Please enter match content @@ -574,7 +389,7 @@ Parse Failed - Could not identify valid node links. Please check and try again. + No valid node links found. Please verify your input and try again. Add Server Manually @@ -600,12 +415,6 @@ Password - - UUID (VMess / VLESS) - - - AlterId (VMess) - Transport Protocol @@ -634,7 +443,7 @@ Local Port - Valid range: {0} - {1} + Range: {0} - {1} Share Node @@ -682,10 +491,10 @@ Empty = none; or mlkem768x25519plus.native.0rtt... - Chain proxy + Add Chain Proxy - Edit chain proxy + Edit Chain Proxy DNS Settings @@ -709,13 +518,13 @@ Direct DNS - For domestic domains (geosite:cn), resolved via direct outbound + For domestic domains (geosite:cn), resolved directly without proxy Proxy DNS - For foreign domains, resolved via proxy outbound to prevent DNS pollution + For foreign domains, resolved via proxy outbound to prevent DNS hijacking TUN mode only @@ -771,7 +580,7 @@ Enable TUN Mode - + Enabling TUN mode requires administrator privileges. The app will restart. Continue? @@ -780,12 +589,6 @@ Not connected - - Copy to clipboard - - - Copied to clipboard - Updating routing data @@ -813,34 +616,6 @@ Updated. Changes will take effect on next xray start. - - xray.exe not found -Path: {0} - - - [Error] - - - [xray process exited] - - - [Start] - - - [Config] - - - xray exited immediately (exit code {0}) - - - [Error] Start failed: - - - [Exception] - - - [Stopped] - Fetching checksum file… @@ -886,18 +661,6 @@ Path: {0} Export Failed - - Downloading via local proxy ({0})… - - - Checking {0}.dat … - - - {0}.dat is already up to date - - - {0}.dat verification failed: downloaded file SHA256 does not match server. - Last updated: never @@ -916,11 +679,8 @@ Path: {0} Last updated: {0} - - Cannot open registry key: - - Local Port + Edit Port Logs @@ -929,10 +689,10 @@ Path: {0} Proxy Setting - Global Proxy + System Proxy - No Proxy Takeover + Bypass System Proxy Startup @@ -941,7 +701,7 @@ Path: {0} Routing - Global + Global Smart @@ -958,7 +718,364 @@ Path: {0} Language - - Choose the app display language + + Server List + + + Add + + + Import Nodes + + + Add Subscription + + + Add Manually + + + Chain Proxy + + + Edit + + + Delete + + + Search servers + + + Filter: + + + Default + + + Active + + + Protocol + + + ● Activated + + + Chain Proxy + + + Favorites + + + Add to Favorites + + + Remove from Favorites + + + Filter + + + Sort + + + Are you sure you want to delete the selected {0} items? + + + Unknown error + + + Server Details + + + Name + + + Protocol + + + Transport + + + AI Unlock Status + + + Latency + + + Entry + + + Exit + + + (entry node missing) + + + (exit node missing) + + + Auth + + + Chain + + + No auth + + + Username/Password + + + {0} ms + + + Proxy Panel + + + Rules + + + Add + + + Global routing is active — custom rules are not in effect. They will apply when switching back to Smart Routing. + + + Save + + + Cancel + + + Advanced edit (open settings.json externally) + + + Edit + + + Delete + + + Could not prepare settings.json + + + Could not open editor + + + No .json file association or launch failed: {0} + + + Type + + + Domain + + + Process + + + Match Content + + + Match by name + + + Match by full path + + + Match entire folder + + + Outbound + + + proxy + + + direct + + + block + + + Please enter match content + + + Browse exe... + + + Browse folder... + + + youtube.com or geosite:cn + + + 192.168.0.0/16 or geoip:cn + + + Type a process name / path / folder, or click Browse + + + Supports exact match, regexp:, geosite: prefixes + + + Supports CIDR and geoip: prefixes + + + By name: matches any exe with this name. By full path: matches only this exe. By folder: matches all exes under the folder. + + + Application language + + + Choose interface language + + + Restart XrayUI to apply language changes + + + Restart + + + Customize the color indicator for each protocol + + + Detail Settings + + + Server info display + + + Choose what to show on the detail card + + + Latency + + + Show latency on the node card + + + AI Unlock + + + Show AI unlock status on the node card + + + Import & Export Configuration + + + Back up the current nodes and routing rules, or restore from a file + + + Export + + + Import + + + Export preset configuration + + + Import preset configuration + + + Exported to {0} + + + Import failed + + + Import successful + + + Imported {0} nodes, {1} subscriptions, {2} rules{3}. + + + , including advanced routing + + + Preset files not found + + + Please prepare servers.json / settings.json under the Import folder first. + + + Name + + + Chain proxy name + + + Entry proxy + + + Select front node + + + Exit proxy + + + Select exit node + + + Traffic is forwarded through the entry proxy and routed out via the exit proxy + + + Please enter a chain proxy name. + + + Please select an entry proxy. + + + Please select an exit proxy. + + + Entry proxy and exit proxy cannot be the same node. + + + More options + + + Maximum transmission unit + + + Outbound interface + + + Xray picks automatically by default; only set this manually if auto mode is unavailable. + + + auto (recommended) + + + Add Subscription + + + Manage Subscriptions + + + Subscription URL + + + Name (optional) + + + Leave empty to use link domain + + + All nodes from the subscription will be parsed and imported automatically + + + No subscriptions + + + Delete subscription? + + + All nodes under this subscription will also be deleted. + + + Delete \ No newline at end of file diff --git a/Strings/zh-CN/Resources.resw b/Strings/zh-CN/Resources.resw index a441cd5..584635d 100644 --- a/Strings/zh-CN/Resources.resw +++ b/Strings/zh-CN/Resources.resw @@ -1,18 +1,96 @@ - + + + + + + + + + + + + + + + + + + + + - - - + + + + @@ -27,392 +105,977 @@ - text/microsoft-resx - 2.0 - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - - 代理控制台 - 切换mini窗口 - 返回完整窗口 - 关闭 - 打开窗口 - 退出 - - - 服务器列表 - 添加 - 编辑 - 删除 - 分享 - 导入节点 - 添加订阅 - 手动添加 - 搜索服务器 - 筛选条件: - 筛选 - 排序 - 默认 - 当前连接 - 协议 - 只在筛选为「所有服务器」时可用 - 所有服务器 - 未分组 - (未命名订阅) - (已删除订阅) - ● 已连接 - - - 启动 - 停止 - 未运行 - 正在应用... - 个性化 - GitHub - 编辑本地端口 - 查看日志 - 代理设置 - 全局代理 - 不接管代理 - 开机启动 - 路由设置 - 全局路由 - 智能分流 - 自定义规则... - TUN 模式 - 路由: - 发现新版本 {0} - - - 服务器详情 - 名称 - 协议 - 地址 - 端口 - 传输 - 加密 - 安全 - AI 访问解锁 - 在浏览器中打开 {0} - 网络延迟 - 重新测试延迟 - 复制分享链接 - 未选择服务器 - 未测试 - 测试中... - 超时 - 失败 - - 个性化设置 - 主题 - 选择应用的外观模式 - 浅色 - 深色 - 跟随系统 - 背景效果 - 选择窗口背景材质 - 亚克力 - 协议颜色 - 重置 - 其他协议 - 未匹配的协议将使用此颜色 - 数据管理 - 导出配置 - 导出 - 完成 - 导出成功 - 已导出至Import文件夹。 - 语言 - 选择应用的显示语言 - 切换语言需要重启应用 - 立即重启 - 稍后 - - - 添加订阅 - 管理订阅 - 订阅链接 - 备注名称(可选) - 留空则使用链接域名 - 将自动拉取并导入订阅中的全部节点 - 暂无订阅 - 刷新订阅 - 删除订阅 - 删除订阅? - 将同时删除该订阅下的所有节点。 - 订阅拉取失败 - 未能从订阅中解析出任何有效节点。 - 请先停止代理后再刷新 - 更新失败: {0} - 请先停止代理后再删除 - - - 自定义路由规则 - 规则列表 - 更新geoFile路由数据 - 添加 - 保存 - 取消 - 编辑 - 删除 - 当前为全局路由模式,自定义规则不生效。切回智能分流后自动启用。 - - - 添加规则 - 添加 - 类型 - 域名(Domain) - 匹配内容 - 支持精确匹配、regexp:、geosite:、geoip: 前缀 - 出站 - proxy(代理) - direct(直连) - block(阻断) - 请填写匹配内容 - - - 代理日志 - 未运行 - 运行中 - ({0} 行) - 日志隐私设置 - IP地址遮罩 - 关闭 - 自动滚动 - 复制全部 - 清除 - 日志隐私设置 - 已保存,当前 TUN 会话下次启动时生效。 - 保存失败:{0} - - - 确定 - 取消 - 保存 - 完成 - 确认 - 添加 - 删除 - 替换 - 正在准备… - - - - - 粘贴节点链接(支持多协议) - 导入节点链接 - 支持常见协议链接 - 解析失败 - 无法识别有效的节点链接,请检查后重试。 - - - 手动添加服务器 - 编辑服务器 - 名称 - 地址 / 域名 - 端口 - 协议 - 加密方式 (SS) - 密码 - UUID (VMess / VLESS) - AlterId (VMess) - 传输协议 - 路径 (WS/gRPC/XHTTP) - Host 头 (WS/XHTTP) - 安全 - 指纹 (uTLS) - 允许不安全连接(跳过证书校验) - xtls-rprx-vision 或留空 - - - 编辑本地端口 - 本地端口 - 有效范围:{0} - {1} - - - 分享节点 - 复制链接 - 不支持分享 - 该节点协议暂不支持生成分享链接。 - - - 开机启动 - 开机自动启动 - 自动连接上次节点 - 开机启动设置失败 - - - 确认删除 - 确定要删除 {0}? - 替换当前配置? - 此操作将覆盖您当前的全部节点和路由规则,强烈建议先导出备份。是否继续? - - - 用户名 (SOCKS) - 例如 udp://1.1.1.1 或服务端生成的 ECHConfig - 留空 = none;或 mlkem768x25519plus.native.0rtt.... - - - 链式代理 - 编辑链式代理 - - - DNS 设置 - DNS设置 - 重置默认 - 输入 IP 或 DoH 地址 (留空为自动) - 查询策略 - 启用 DNS 缓存 - 直连 DNS - 用于国内域名 (geosite:cn),走直连出站解析 - 代理 DNS - 用于境外域名,经代理出站解析,防止 DNS 污染 - 仅TUN模式有效 - 实验性 - 阿里 - 腾讯 - 谷歌 - 仅 IPv4 - 仅 IPv6 - 自动 - - - 未选择服务器 - 请先从列表中选择服务器 - 启动失败 - xray 启动失败. 请检查服务器配置. - 应用新配置失败 - xray 应用新配置失败,已停止。 - 更新失败 - 无法启动升级器:{0} - 正在更新 XrayUI - - - 开启TUN模式 - 开启 TUN 模式需要管理员权限,程序将会重启,是否继续? - - - 未选择 - 未连接 - - - 复制到剪贴板 - 已复制到剪贴板 - - - - 正在更新路由数据 - 已是最新 - geoip.dat 和 geosite.dat 都已是最新版本,无需下载。 - 更新成功 - 已更新。TUN 模式下请手动重启以生效。 - 已更新并重新加载 xray。 - 已更新数据文件,但重启 xray 失败:{0} - 已更新。请重启 xray 以生效。 - 已更新。下次启动 xray 时生效。 - - - 找不到 xray.exe -路径:{0} - [错误] - [xray 进程已退出] - [启动] - [配置] - xray 立即退出(退出码 {0}) - [错误] 启动失败: - [异常] - [已停止] - - - 正在获取校验文件… - 更新校验文件格式异常 - 无法下载更新校验文件: - 更新包校验失败:SHA256 与服务器公布的不一致。 - 正在解压更新包… - 更新包解压失败: - 正在验证更新包… - 更新包内容异常:缺少必要文件。 - 更新包版本不匹配:期望 {0},实际 {1}。 - 缺少升级器组件,请重新下载完整安装包。 - 正在准备重启… - 正在下载 {0} … {1:0.0} / {2:0.0} MB - 正在下载 {0} … {1:0.0} MB - - - 编辑规则 - - - 导出失败 - - - 通过本地代理下载({0})… - 正在检查 {0}.dat … - {0}.dat 已是最新 - {0}.dat 校验失败:下载文件的 SHA256 与服务器公布的不一致。 - - - 上次更新: 从未更新 - 刚刚 - {0} 分钟前 - {0} 小时前 - {0} 天前 - 上次更新: {0} - - - 无法打开注册表项: - - - - 编辑本地端口 - 查看日志 - 代理设置 - 全局代理 - 不接管代理 - 开机启动 - 路由设置 - 全局路由 - 智能分流 - 自定义规则... - TUN 模式 - 路由: - - - 语言 - 选择应用的显示语言 - - + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + 代理控制台 + + + 切换mini窗口 + + + 返回完整窗口 + + + 关闭 + + + 打开窗口 + + + 退出 + + + 编辑 + + + 删除 + + + 分享 + + + 只在筛选为「所有服务器」时可用 + + + 所有服务器 + + + 未分组 + + + (未命名订阅) + + + (已删除订阅) + + + 启动 + + + 停止 + + + 未运行 + + + 正在应用... + + + 个性化 + + + 全局路由 + + + 智能分流 + + + 发现新版本 {0} + + + 地址 + + + 端口 + + + 加密 + + + 安全 + + + 在浏览器中打开 {0} + + + 重新测试延迟 + + + 复制分享链接 + + + 未选择服务器 + + + 未测试 + + + 测试中... + + + 超时 + + + 失败 + + + 个性化设置 + + + 主题 + + + 浅色 + + + 深色 + + + 跟随系统 + + + 背景效果 + + + 亚克力 + + + 协议颜色 + + + 重置 + + + 其他协议 + + + 未匹配的协议将使用此颜色 + + + 数据管理 + + + 完成 + + + 导出成功 + + + 跟随系统 + Display name of the "follow system" language row — referenced by LanguageInfo.DisplayName when Tag is null. + + + 添加订阅 + + + 管理订阅 + + + 刷新订阅 + + + 删除订阅 + + + 订阅拉取失败 + + + 未能从订阅中解析出任何有效节点。 + + + 请先停止代理后再刷新 + + + 更新失败: {0} + + + 请先停止代理后再删除 + + + 自定义路由规则 + + + 更新geoFile路由数据 + + + 添加规则 + + + 请填写匹配内容 + + + 代理日志 + + + 未运行 + + + 运行中 + + + ({0} 行) + + + 日志隐私设置 + + + IP地址遮罩 + + + 关闭 + + + 自动滚动 + + + 复制全部 + + + 清除 + + + 日志隐私设置 + + + 已保存,当前 TUN 会话下次启动时生效。 + + + 保存失败:{0} + + + 确定 + + + 取消 + + + 保存 + + + 完成 + + + 确认 + + + 添加 + + + 删除 + + + 替换 + + + 正在准备… + + + + + + + + + 粘贴节点链接(支持多协议) + + + 导入节点链接 + + + 支持常见协议链接 + + + 解析失败 + + + 无法识别有效的节点链接,请检查后重试。 + + + 手动添加服务器 + + + 编辑服务器 + + + 名称 + + + 地址 / 域名 + + + 端口 + + + 协议 + + + 加密方式 (SS) + + + 密码 + + + 传输协议 + + + 路径 (WS/gRPC/XHTTP) + + + Host 头 (WS/XHTTP) + + + 安全 + + + 指纹 (uTLS) + + + 允许不安全连接(跳过证书校验) + + + xtls-rprx-vision 或留空 + + + 编辑本地端口 + + + 本地端口 + + + 有效范围:{0} - {1} + + + 分享节点 + + + 复制链接 + + + 不支持分享 + + + 该节点协议暂不支持生成分享链接。 + + + 开机启动 + + + 开机自动启动 + + + 自动连接上次节点 + + + 开机启动设置失败 + + + 确认删除 + + + 确定要删除 {0}? + + + 替换当前配置? + + + 此操作将覆盖您当前的全部节点和路由规则,强烈建议先导出备份。是否继续? + + + 用户名 (SOCKS) + + + 例如 udp://1.1.1.1 或服务端生成的 ECHConfig + + + 留空 = none;或 mlkem768x25519plus.native.0rtt.... + + + 链式代理 + + + 编辑链式代理 + + + DNS 设置 + + + DNS设置 + + + 重置默认 + + + 输入 IP 或 DoH 地址 (留空为自动) + + + 查询策略 + + + 启用 DNS 缓存 + + + 直连 DNS + + + 用于国内域名 (geosite:cn),走直连出站解析 + + + 代理 DNS + + + 用于境外域名,经代理出站解析,防止 DNS 污染 + + + 仅TUN模式有效 + + + 实验性 + + + 阿里 + + + 腾讯 + + + 谷歌 + + + 仅 IPv4 + + + 仅 IPv6 + + + 自动 + + + 未选择服务器 + + + 请先从列表中选择服务器 + + + 启动失败 + + + xray 启动失败. 请检查服务器配置. + + + 应用新配置失败 + + + xray 应用新配置失败,已停止。 + + + 更新失败 + + + 无法启动升级器:{0} + + + 正在更新 XrayUI + + + 开启TUN模式 + + + 开启 TUN 模式需要管理员权限,程序将会重启,是否继续? + + + 未选择 + + + 未连接 + + + 正在更新路由数据 + + + 已是最新 + + + geoip.dat 和 geosite.dat 都已是最新版本,无需下载。 + + + 更新成功 + + + 已更新。TUN 模式下请手动重启以生效。 + + + 已更新并重新加载 xray。 + + + 已更新数据文件,但重启 xray 失败:{0} + + + 已更新。请重启 xray 以生效。 + + + 已更新。下次启动 xray 时生效。 + + + 正在获取校验文件… + + + 更新校验文件格式异常 + + + 无法下载更新校验文件: + + + 更新包校验失败:SHA256 与服务器公布的不一致。 + + + 正在解压更新包… + + + 更新包解压失败: + + + 正在验证更新包… + + + 更新包内容异常:缺少必要文件。 + + + 更新包版本不匹配:期望 {0},实际 {1}。 + + + 缺少升级器组件,请重新下载完整安装包。 + + + 正在准备重启… + + + 正在下载 {0} … {1:0.0} / {2:0.0} MB + + + 正在下载 {0} … {1:0.0} MB + + + 编辑规则 + + + 导出失败 + + + 上次更新: 从未更新 + + + 刚刚 + + + {0} 分钟前 + + + {0} 小时前 + + + {0} 天前 + + + 上次更新: {0} + + + 编辑本地端口 + + + 查看日志 + + + 代理设置 + + + 全局代理 + + + 不接管代理 + + + 开机启动 + + + 路由设置 + + + 全局路由 + + + 智能分流 + + + 自定义规则... + + + TUN 模式 + + + 路由: + + + 语言 + + + 服务器列表 + + + 添加 + + + 导入节点 + + + 添加订阅 + + + 手动添加 + + + 链式代理 + + + 编辑 + + + 删除 + + + 搜索服务器 + + + 筛选条件: + + + 默认 + + + 当前连接 + + + 协议 + + + ● 已连接 + + + 链式代理 + + + 收藏列表 + + + 加入收藏 + + + 取消收藏 + + + 筛选 + + + 排序 + + + 确定要删除当前 {0} 个项目? + + + 未知错误 + + + 服务器详情 + + + 名称 + + + 协议 + + + 传输 + + + AI 访问解锁 + + + 网络延迟 + + + 入口 + + + 出口 + + + (入口节点缺失) + + + (出口节点缺失) + + + 认证 + + + 链路 + + + 无认证 + + + 用户名/密码 + + + {0} ms + + + 代理控制台 + + + 规则列表 + + + 添加 + + + 当前为全局路由模式,自定义规则不生效。切回智能分流后自动启用。 + + + 保存 + + + 取消 + + + 高级编辑 (用外部编辑器打开 settings.json) + + + 编辑 + + + 删除 + + + 无法准备 settings.json + + + 无法打开编辑器 + + + 未找到 .json 关联程序或启动失败:{0} + + + 类型 + + + 域名(Domain) + + + 进程(Process) + + + 匹配内容 + + + 匹配同名进程 + + + 匹配完整路径 + + + 匹配整个目录 + + + 出站 + + + proxy(代理) + + + direct(直连) + + + block(阻断) + + + 请填写匹配内容 + + + 浏览 exe... + + + 浏览文件夹... + + + youtube.com 或 geosite:cn + + + 192.168.0.0/16 或 geoip:cn + + + 可手填进程名 / 路径 / 文件夹,或点浏览选取 + + + 支持精确匹配、regexp:、geosite: 前缀 + + + 支持 CIDR 与 geoip: 前缀 + + + 同名进程:匹配任意路径下的同名 exe;完整路径:只匹配此 exe;整个目录:匹配该目录下所有 exe + + + 应用语言 + + + 选择应用界面的显示语言 + + + 重启应用以应用语言更改 + + + 重启 + + + 自定义不同协议的颜色标识 + + + 详情设置 + + + 服务器信息显示 + + + 选择详情卡片上的信息展示 + + + 延迟 + + + 节点卡片上展示延迟 + + + AI 解锁 + + + 节点卡片上展示AI的解锁状态 + + + 配置导入与导出 + + + 将当前的节点和路由规则备份到本地,或从已有文件中恢复 + + + 导出 + + + 导入 + + + 导出预置配置 + + + 导入预置配置 + + + 已导出至 {0} + + + 导入失败 + + + 导入成功 + + + 已导入 {0} 个节点、{1} 条订阅、{2} 条规则{3}。 + + + 、含高级路由 + + + 未找到预置文件 + + + 请先准备 Import 文件夹下的 servers.json / settings.json。 + + + 名称 + + + 链式代理名称 + + + 入口代理 + + + 选择前置节点 + + + 出口代理 + + + 选择落地节点 + + + 流量经入口代理转发,再由出口代理出站 + + + 请输入链式代理名称。 + + + 请选择入口代理。 + + + 请选择出口代理。 + + + 入口代理和出口代理不能是同一个节点。 + + + 更多选项 + + + 最大传输单元 + + + 出口网卡 + + + 默认使用 Xray 自动选择;只有自动模式不可用时才手动指定。 + + + auto(推荐) + + + 添加订阅 + + + 管理订阅 + + + 订阅链接 + + + 备注名称(可选) + + + 留空则使用链接域名 + + + 将自动解析并导入该订阅中的全部节点 + + + 暂无订阅 + + + 删除订阅? + + + 将同时删除该订阅下的所有节点。 + + + 删除 + + \ No newline at end of file diff --git a/ViewModels/CustomRulesViewModel.cs b/ViewModels/CustomRulesViewModel.cs index e4660fe..b8e5436 100644 --- a/ViewModels/CustomRulesViewModel.cs +++ b/ViewModels/CustomRulesViewModel.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Linq; using System.Threading.Tasks; +using XrayUI.Helpers; using XrayUI.Models; using XrayUI.Services; @@ -187,7 +188,7 @@ private async Task OpenAdvancedEditor() catch (Exception ex) { await _dialogs.ShowErrorAsync( - "无法准备 settings.json", + L.CustomRules_PrepFailedTitle, ex.Message, xamlRoot); return; @@ -201,8 +202,8 @@ await _dialogs.ShowErrorAsync( catch (Exception ex) { await _dialogs.ShowErrorAsync( - "无法打开编辑器", - $"未找到 .json 关联程序或启动失败:{ex.Message}", + L.CustomRules_OpenEditorFailedTitle, + Loc.Format("CustomRules_OpenEditorFailedMsg", ex.Message), xamlRoot); } } @@ -230,7 +231,7 @@ private async Task UpdateGeoData() try { await _dialogs.ShowProgressDialogAsync( - "正在更新路由数据", + L.GeoUpdate_Updating, async (progress, ct) => result = await _geoUpdate.UpdateAsync(progress, proxyUrl, ct), xamlRoot); } @@ -243,7 +244,7 @@ await _dialogs.ShowProgressDialogAsync( } catch (Exception ex) { - await _dialogs.ShowErrorAsync("更新失败", ex.Message, xamlRoot); + await _dialogs.ShowErrorAsync(L.Error_UpdateFailed, ex.Message, xamlRoot); return; } @@ -251,8 +252,8 @@ await _dialogs.ShowProgressDialogAsync( if (!result.AnyUpdated) { await _dialogs.ShowErrorAsync( - "已是最新", - "geoip.dat 和 geosite.dat 都已是最新版本,无需下载。", + L.GeoUpdate_AlreadyLatest, + L.GeoUpdate_AlreadyLatestMsg, xamlRoot); return; } @@ -263,31 +264,31 @@ await _dialogs.ShowErrorAsync( { if (_isTunMode?.Invoke() == true) { - message = "已更新。TUN 模式下请手动重启以生效。"; + message = L.GeoUpdate_TunRestart; } else if (_reapplyRouting != null) { try { await _reapplyRouting(); - message = "已更新并重新加载 xray。"; + message = L.GeoUpdate_ReloadedOk; } catch (Exception ex) { - message = $"已更新数据文件,但重启 xray 失败:{ex.Message}"; + message = Loc.Format("GeoUpdate_ReloadFailed", ex.Message); } } else { - message = "已更新。请重启 xray 以生效。"; + message = L.GeoUpdate_RestartRequired; } } else { - message = "已更新。下次启动 xray 时生效。"; + message = L.GeoUpdate_NextStart; } - await _dialogs.ShowErrorAsync("更新成功", message, xamlRoot); + await _dialogs.ShowErrorAsync(L.GeoUpdate_Success, message, xamlRoot); } } } diff --git a/ViewModels/ManageSubscriptionsViewModel.cs b/ViewModels/ManageSubscriptionsViewModel.cs index 934e729..a620f16 100644 --- a/ViewModels/ManageSubscriptionsViewModel.cs +++ b/ViewModels/ManageSubscriptionsViewModel.cs @@ -3,6 +3,7 @@ using System.Collections.ObjectModel; using System.Collections.Specialized; using System.Threading.Tasks; +using XrayUI.Helpers; using XrayUI.Models; namespace XrayUI.ViewModels @@ -74,7 +75,7 @@ private void OnCollectionChanged(object? sender, NotifyCollectionChangedEventArg public bool IsAddPage => SelectedIndex == 0; public bool IsManagePage => SelectedIndex == 1; public bool CanAddSubscription => IsAddPage && !string.IsNullOrWhiteSpace(SubscriptionUrl); - public string DialogTitle => IsAddPage ? "添加订阅" : "管理订阅"; + public string DialogTitle => IsAddPage ? L.Subscription_DialogTitle_Add : L.Subscription_DialogTitle_Manage; public Visibility AddPageVisibility => IsAddPage ? Visibility.Visible : Visibility.Collapsed; public Visibility ManagePageVisibility => IsManagePage ? Visibility.Visible : Visibility.Collapsed; diff --git a/ViewModels/ServerDetailViewModel.cs b/ViewModels/ServerDetailViewModel.cs index d05426a..ac430cd 100644 --- a/ViewModels/ServerDetailViewModel.cs +++ b/ViewModels/ServerDetailViewModel.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.UI.Xaml.Media; +using XrayUI.Helpers; using XrayUI.Models; using XrayUI.Services; @@ -26,7 +27,7 @@ private static SolidColorBrush GetBrush(string key) => private AiUnlockStatus? _claudeStatus; private AiUnlockStatus? _geminiStatus; private ServerEntry? _selectedServer; - private string _latencyText = "Not tested"; + private string _latencyText = string.Empty; private bool _isTestingLatency; private SolidColorBrush _openAiStatusBrush = null!; private SolidColorBrush _claudeStatusBrush = null!; @@ -38,6 +39,7 @@ public ServerDetailViewModel(LatencyProbeService latencyProbe, AiUnlockCheckServ { _latencyProbe = latencyProbe; _aiUnlockCheck = aiUnlockCheck; + _latencyText = L.ServerDetail_NotTested; ResetAiUnlockDisplay(); } @@ -89,22 +91,22 @@ private set } } - public string SelectedName => SelectedServer?.Name ?? "未选择服务器"; + public string SelectedName => SelectedServer?.Name ?? L.ServerDetail_NoServer; public string SelectedHostLabel - => SelectedServer?.IsChain == true ? "入口" : "地址"; + => SelectedServer?.IsChain == true ? L.ServerDetail_Entry : L.ServerDetail_Address; public string SelectedHost => SelectedServer?.IsChain == true - ? ResolveChainServer(SelectedServer.ChainEntryServerId)?.Name ?? "(入口节点缺失)" + ? ResolveChainServer(SelectedServer.ChainEntryServerId)?.Name ?? L.ServerDetail_EntryMissing : SelectedServer?.Host ?? "-"; public string SelectedPortLabel - => SelectedServer?.IsChain == true ? "出口" : "端口"; + => SelectedServer?.IsChain == true ? L.ServerDetail_Exit : L.ServerDetail_Port; public string SelectedPort => SelectedServer?.IsChain == true - ? ResolveChainServer(SelectedServer.ChainExitServerId)?.Name ?? "(出口节点缺失)" + ? ResolveChainServer(SelectedServer.ChainExitServerId)?.Name ?? L.ServerDetail_ExitMissing : SelectedServer?.Port.ToString() ?? "-"; public string SelectedProtocol => SelectedServer?.DisplayProtocol ?? "-"; @@ -112,10 +114,10 @@ public string SelectedPort public string SelectedSecurityLabel => (SelectedServer?.Protocol?.ToLowerInvariant()) switch { - "ss" => "加密", - "socks" => "认证", - "chain" => "链路", - _ => "安全" + "ss" => L.ServerDetail_Encryption, + "socks" => L.ServerDetail_AuthLabel, + "chain" => L.ServerDetail_ChainLabel, + _ => L.ServerDetail_Security }; public string SelectedEncryption @@ -128,8 +130,8 @@ public string SelectedEncryption case "socks": return string.IsNullOrWhiteSpace(SelectedServer.Username) && string.IsNullOrWhiteSpace(SelectedServer.Password) - ? "无认证" - : "用户名/密码"; + ? L.ServerDetail_NoAuth + : L.ServerDetail_UserPass; case "chain": var entry = ResolveChainServer(SelectedServer.ChainEntryServerId)?.DisplayProtocol ?? "?"; var exit = ResolveChainServer(SelectedServer.ChainExitServerId)?.DisplayProtocol ?? "?"; @@ -356,8 +358,7 @@ private void NotifySelectedServerFieldsChanged() private void ResetLatencyDisplay() { - LatencyText = "未测试"; - + LatencyText = L.ServerDetail_NotTested; } private void ResetAiUnlockDisplay() @@ -498,7 +499,7 @@ private async Task TestLatency() _latencyTestCts = cts; IsTestingLatency = true; - LatencyText = "测试中..."; + LatencyText = L.ServerDetail_Testing; try { @@ -514,9 +515,9 @@ private async Task TestLatency() LatencyText = result.Status switch { - LatencyProbeStatus.Success => $"{result.Milliseconds ?? 0} ms", - LatencyProbeStatus.Timeout => "超时", - _ => "失败" + LatencyProbeStatus.Success => Loc.Format("ServerDetail_LatencyMs", result.Milliseconds ?? 0), + LatencyProbeStatus.Timeout => L.ServerDetail_Timeout, + _ => L.ServerDetail_Failed }; } catch (OperationCanceledException) when (cts.IsCancellationRequested) diff --git a/ViewModels/ServerListViewModel.cs b/ViewModels/ServerListViewModel.cs index 847cb79..89e3d65 100644 --- a/ViewModels/ServerListViewModel.cs +++ b/ViewModels/ServerListViewModel.cs @@ -27,13 +27,15 @@ public partial class ServerListViewModel : ObservableObject, IDisposable private const string AllChipKey = "__all__"; private const string UngroupedChipKey = "__ungrouped__"; private const string FavoritesChipKey = "__favorites__"; - private const string AllChipName = "所有服务器"; - private const string UngroupedName = "未分组"; - private const string FavoritesName = "收藏列表"; - private const string UnnamedSubLabel = "(未命名订阅)"; - private const string OrphanSubLabel = "(已删除订阅)"; private const string SubscriptionUserAgent = "v2rayN/7.0"; + // Localized labels — looked up lazily so language changes apply at startup. + private static string AllChipName => L.ServerList_AllServers; + private static string UngroupedName => L.ServerList_Ungrouped; + private static string FavoritesName => L.ServerList_Favorites; + private static string UnnamedSubLabel => L.ServerList_UnnamedSub; + private static string OrphanSubLabel => L.ServerList_OrphanSub; + private static readonly HttpClient Http = CreateSubscriptionHttpClient(); private static HttpClient CreateSubscriptionHttpClient() @@ -613,7 +615,7 @@ private async Task ImportFromLink() if (added == 0) { - await _dialogs.ShowErrorAsync("解析失败", "无法识别有效的节点链接,请检查后重试。"); + await _dialogs.ShowErrorAsync(L.Import_ParseFailed, L.Import_ParseFailedMsg); return; } @@ -664,7 +666,7 @@ private async Task OpenSubscriptions() if (entries == null) { - await _dialogs.ShowErrorAsync("订阅拉取失败", error ?? "未知错误"); + await _dialogs.ShowErrorAsync(L.Subscription_FetchFailed, error ?? L.Subscription_UnknownError); } } @@ -699,7 +701,7 @@ private async Task OpenSubscriptions() } if (entries.Count == 0) - return (null, "未能从订阅中解析出任何有效节点。"); + return (null, L.Subscription_NoParsed); return (entries, null); } @@ -708,7 +710,7 @@ private async Task RefreshSubscriptionAsync(SubscriptionEntry sub) { if (IsSubscriptionLocked(sub.Id)) { - sub.LastError = "请先停止代理后再刷新"; + sub.LastError = L.Subscription_StopFirst_Refresh; return; } @@ -718,7 +720,7 @@ private async Task RefreshSubscriptionAsync(SubscriptionEntry sub) var (newEntries, error) = await FetchSubscriptionNodesAsync(sub); if (newEntries == null) { - sub.LastError = $"更新失败: {error}"; + sub.LastError = Loc.Format("Subscription_UpdateFailed", error); return; } @@ -803,7 +805,7 @@ private async Task DeleteSubscriptionAsync(SubscriptionEntry sub) { if (IsSubscriptionLocked(sub.Id)) { - sub.LastError = "请先停止代理后再删除"; + sub.LastError = L.Subscription_StopFirst_Delete; return false; } @@ -937,7 +939,7 @@ private async Task ShareServer() var link = NodeLinkSerializer.ToLink(SelectedServer); if (string.IsNullOrEmpty(link)) { - await _dialogs.ShowErrorAsync("不支持分享", "该节点协议暂不支持生成分享链接。"); + await _dialogs.ShowErrorAsync(L.Share_NotSupported, L.Share_NotSupportedMsg); return; } @@ -1000,14 +1002,14 @@ private async Task RemoveServer() var isBatchDelete = selectedServers.Count > 1; var message = isBatchDelete - ? $"确定要删除当前 {selectedServers.Count} 个项目?" - : $"确定要删除 {selectedServers[0].Name}?"; + ? Loc.Format("Confirm_DeleteBatchMsg", selectedServers.Count) + : Loc.Format("Confirm_DeleteMsg", selectedServers[0].Name); var confirmed = await _dialogs.ShowConfirmationAsync( - "确认删除", + L.Confirm_DeleteTitle, message, - "删除", - "取消", + L.Dialog_Delete, + L.Dialog_Cancel, isDanger: true); if (!confirmed) return; diff --git a/Views/AddChainProxyDialog.xaml b/Views/AddChainProxyDialog.xaml index 4a206ad..efa5530 100644 --- a/Views/AddChainProxyDialog.xaml +++ b/Views/AddChainProxyDialog.xaml @@ -11,18 +11,22 @@ - - @@ -49,10 +53,12 @@ - @@ -76,7 +82,8 @@ - diff --git a/Views/AddChainProxyDialog.xaml.cs b/Views/AddChainProxyDialog.xaml.cs index 52372c9..6274070 100644 --- a/Views/AddChainProxyDialog.xaml.cs +++ b/Views/AddChainProxyDialog.xaml.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq; +using XrayUI.Helpers; using XrayUI.Models; namespace XrayUI.Views @@ -39,25 +40,25 @@ public bool TryCreateOrUpdate(out ServerEntry? entry) var name = NameTextBox.Text.Trim(); if (string.IsNullOrWhiteSpace(name)) { - ShowError("请输入链式代理名称。"); + ShowError(L.ChainProxy_NameRequired); return false; } if (EntryComboBox.SelectedItem is not ServerEntry entryServer) { - ShowError("请选择入口代理。"); + ShowError(L.ChainProxy_EntryRequired); return false; } if (ExitComboBox.SelectedItem is not ServerEntry exitServer) { - ShowError("请选择出口代理。"); + ShowError(L.ChainProxy_ExitRequired); return false; } if (entryServer.Id == exitServer.Id) { - ShowError("入口代理和出口代理不能是同一个节点。"); + ShowError(L.ChainProxy_EntryExitSame); return false; } diff --git a/Views/AddRuleDialog.xaml b/Views/AddRuleDialog.xaml index e9528c0..feaac11 100644 --- a/Views/AddRuleDialog.xaml +++ b/Views/AddRuleDialog.xaml @@ -14,20 +14,22 @@ - - - - + + + - - - - + + + @@ -61,19 +63,21 @@ - - - - + + + "192.168.0.0/16 或 geoip:cn", - "process" => "可手填进程名 / 路径 / 文件夹,或点浏览选取", - _ => "youtube.com 或 geosite:cn", + "ip" => L.AddRule_PlaceholderIp, + "process" => L.AddRule_PlaceholderProcess, + _ => L.AddRule_PlaceholderDomain, }; } @@ -81,9 +85,9 @@ private void ApplyTypeUiState() { HintTextBlock.Text = tag switch { - "ip" => "支持 CIDR 与 geoip: 前缀", - "process" => "同名进程:匹配任意路径下的同名 exe;完整路径:只匹配此 exe;整个目录:匹配该目录下所有 exe", - _ => "支持精确匹配、regexp:、geosite: 前缀", + "ip" => L.AddRule_HintIp, + "process" => L.AddRule_HintProcess, + _ => L.AddRule_HintDomain, }; } } @@ -97,7 +101,7 @@ private void ApplyBrowseFormatUiState() { if (BrowseButtonText is null) return; var isFolder = GetSelectedBrowseFormat() == "folder"; - BrowseButtonText.Text = isFolder ? "浏览文件夹..." : "浏览 exe..."; + BrowseButtonText.Text = isFolder ? L.AddRule_BrowseFolder : L.AddRule_BrowseExe; if (BrowseButtonIcon is not null) { BrowseButtonIcon.Glyph = isFolder ? "\uE8DA" : "\uE8E5"; diff --git a/Views/ControlPanelControl.xaml b/Views/ControlPanelControl.xaml index 9252a68..3383e29 100644 --- a/Views/ControlPanelControl.xaml +++ b/Views/ControlPanelControl.xaml @@ -23,12 +23,12 @@ VerticalAlignment="Center" Spacing="2"> - diff --git a/Views/ControlPanelControl.xaml.cs b/Views/ControlPanelControl.xaml.cs index 889ffc4..b373537 100644 --- a/Views/ControlPanelControl.xaml.cs +++ b/Views/ControlPanelControl.xaml.cs @@ -1,5 +1,6 @@ using System; using Windows.System; +using XrayUI.Helpers; namespace XrayUI.Views { @@ -13,6 +14,7 @@ public sealed partial class ControlPanelControl public ControlPanelControl() { this.InitializeComponent(); + ToolTipService.SetToolTip(PersonalizeButton, L.ControlPanel_Personalize); } // Called by MainWindow after ViewModel is assigned (via x:Bind the property is set before Loaded) diff --git a/Views/CustomRulesWindow.xaml b/Views/CustomRulesWindow.xaml index b535724..00bb65b 100644 --- a/Views/CustomRulesWindow.xaml +++ b/Views/CustomRulesWindow.xaml @@ -24,7 +24,8 @@ - @@ -36,7 +37,6 @@ Background="Transparent" BorderThickness="0" VerticalAlignment="Center" - ToolTipService.ToolTip="高级编辑 (用外部编辑器打开 settings.json)" Command="{x:Bind ViewModel.OpenAdvancedEditorCommand}"> @@ -45,7 +45,6 @@ Background="Transparent" BorderThickness="0" VerticalAlignment="Center" - ToolTipService.ToolTip="更新GeoFiles路由数据" Click="UpdateGeoButton_Click"> @@ -53,7 +52,7 @@ Command="{x:Bind ViewModel.AddRuleCommand}"> - + @@ -73,7 +72,8 @@ FontSize="14" Foreground="{ThemeResource SystemFillColorCautionBrush}" VerticalAlignment="Center" /> - @@ -154,14 +154,14 @@ @@ -179,11 +179,13 @@ - @@ -99,6 +104,7 @@ - + VerticalAlignment="Center"> @@ -146,7 +152,8 @@ - @@ -163,24 +170,27 @@ - + BorderBrush="Transparent"> - - - + @@ -227,7 +237,8 @@ Visibility="{x:Bind IsActive, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}" Grid.Column="1" Margin="8,0,0,0"> - @@ -248,7 +259,8 @@ FontWeight="SemiBold" Foreground="{x:Bind Protocol, Mode=OneWay, Converter={StaticResource ProtocolToBrushConverter}}" /> - ViewModel.EditServerCommand.Execute(null); var isFavorite = ViewModel.SelectedServer?.IsFavorite == true; var favoriteItem = CreateMenuItem( - isFavorite ? "取消收藏" : "加入收藏", + isFavorite ? L.ServerList_RemoveFavorite : L.ServerList_AddFavorite, isFavorite ? "\uE8D9" : "\uE734"); favoriteItem.Click += (_, _) => ViewModel.ToggleFavoriteCommand.Execute(null); - var deleteItem = CreateMenuItem("删除", "\uE74D"); + var deleteItem = CreateMenuItem(L.ServerList_Delete, ""); deleteItem.IsEnabled = ViewModel.CanRemoveSelectedServer; deleteItem.Click += (_, _) => ViewModel.RemoveServerCommand.Execute(null); - var shareItem = CreateMenuItem("分享", "\uE72D"); + var shareItem = CreateMenuItem(L.ServerList_Share, ""); shareItem.Click += (_, _) => ViewModel.ShareServerCommand.Execute(null); flyout.Items.Add(editItem); diff --git a/Views/TunConfirmationDialog.xaml b/Views/TunConfirmationDialog.xaml index 370d775..7be5ee2 100644 --- a/Views/TunConfirmationDialog.xaml +++ b/Views/TunConfirmationDialog.xaml @@ -10,9 +10,11 @@ - @@ -46,7 +49,8 @@ - @@ -54,8 +58,7 @@ + SelectedIndex="0" /> diff --git a/Views/TunConfirmationDialog.xaml.cs b/Views/TunConfirmationDialog.xaml.cs index d4e5580..72e97d1 100644 --- a/Views/TunConfirmationDialog.xaml.cs +++ b/Views/TunConfirmationDialog.xaml.cs @@ -4,17 +4,20 @@ using System.Net.NetworkInformation; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using XrayUI.Helpers; using XrayUI.Services; namespace XrayUI.Views { public sealed partial class TunConfirmationDialog : UserControl { - private const string AutoInterfaceLabel = "auto(推荐)"; + // Localized at construction so the resource loader is already initialized. + private static string AutoInterfaceLabel => L.Tun_AutoInterfaceLabel; public TunConfirmationDialog(int currentMtu, string currentInterface) { this.InitializeComponent(); + ToolTipService.SetToolTip(InterfaceComboBox, L.Tun_InterfaceTooltip); MtuNumberBox.Value = currentMtu; PopulateInterfaceComboBox(currentInterface); } @@ -31,7 +34,7 @@ private void MoreOptionsButton_Click(object sender, RoutedEventArgs e) if (AdvancedSettingsPanel.Visibility == Visibility.Visible) { AdvancedSettingsPanel.Visibility = Visibility.Collapsed; - MoreOptionsButton.Content = "更多选项"; + MoreOptionsButton.Content = L.Tun_MoreOptions; } else { From cf0a93de025cacf407424015581d55447cf4a051 Mon Sep 17 00:00:00 2001 From: Zero <1270128439@qq.com> Date: Sat, 23 May 2026 22:15:10 +0800 Subject: [PATCH 6/6] fix(i18n): clear override for follow-system language --- App.xaml.cs | 18 +++++++++++------- Helpers/LanguageHelper.cs | 12 ++++++------ 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/App.xaml.cs b/App.xaml.cs index 5bf1ca4..c355a95 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -40,22 +40,26 @@ public App() private static void ApplyPersistedLanguageOverride() { + string? language = null; try { - if (!File.Exists(AppPaths.SettingsJsonPath)) return; - - using var stream = File.OpenRead(AppPaths.SettingsJsonPath); - using var doc = JsonDocument.Parse(stream); - if (doc.RootElement.TryGetProperty("Language", out var langElem) - && langElem.ValueKind == JsonValueKind.String) + if (File.Exists(AppPaths.SettingsJsonPath)) { - LanguageHelper.ApplyOverride(langElem.GetString()); + using var stream = File.OpenRead(AppPaths.SettingsJsonPath); + using var doc = JsonDocument.Parse(stream); + if (doc.RootElement.TryGetProperty("Language", out var langElem) + && langElem.ValueKind == JsonValueKind.String) + { + language = langElem.GetString(); + } } } catch (Exception ex) { Debug.WriteLine($"[Language] Failed to load persisted language: {ex.Message}"); } + + LanguageHelper.ApplyOverride(language); } protected override async void OnLaunched(LaunchActivatedEventArgs args) diff --git a/Helpers/LanguageHelper.cs b/Helpers/LanguageHelper.cs index 400b3e9..59efe51 100644 --- a/Helpers/LanguageHelper.cs +++ b/Helpers/LanguageHelper.cs @@ -95,22 +95,22 @@ public static int IndexOf(string? tag) : null; /// - /// Sets (or, when is null / unsupported, leaves - /// alone) the WinAppSDK language override. Must be called before any XAML - /// resource resolution — i.e. before App.InitializeComponent. + /// Applies the WinAppSDK language override. null / unsupported means + /// "follow system", which must explicitly clear any previously-persisted + /// override. Must be called before any XAML resource resolution. /// public static void ApplyOverride(string? tag) { var language = Normalize(tag); - if (string.IsNullOrEmpty(language)) return; try { - ApplicationLanguages.PrimaryLanguageOverride = language; + ApplicationLanguages.PrimaryLanguageOverride = language ?? string.Empty; } catch (Exception ex) { - Debug.WriteLine($"[Language] Failed to apply '{language}': {ex.Message}"); + var label = string.IsNullOrEmpty(language) ? "follow system" : language; + Debug.WriteLine($"[Language] Failed to apply '{label}': {ex.Message}"); } } }