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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Views/PersonalizeControl.xaml.cs b/Views/PersonalizeControl.xaml.cs
index d129049..a6a454f 100644
--- a/Views/PersonalizeControl.xaml.cs
+++ b/Views/PersonalizeControl.xaml.cs
@@ -56,5 +56,11 @@ private void ShowInfo(InfoBarSeverity severity, string title, string message)
OperationInfoBar.Message = message;
OperationInfoBar.IsOpen = true;
}
+
+ private async void LanguageRestartButton_Click(object sender, RoutedEventArgs e)
+ {
+ await ViewModel.ApplyLanguageAsync();
+ App.Restart();
+ }
}
}
diff --git a/XrayUI-dev.csproj b/XrayUI-dev.csproj
index 37c77d8..d281658 100644
--- a/XrayUI-dev.csproj
+++ b/XrayUI-dev.csproj
@@ -11,6 +11,10 @@
true
false
true
+
+ en-US
enable
true
None
From b50c7dfef24b04f383fbd77756c771e5f418d2ec Mon Sep 17 00:00:00 2001
From: Zero <1270128439@qq.com>
Date: Sat, 23 May 2026 09:21:00 +0800
Subject: [PATCH 2/6] fix(deps): pin Microsoft.WindowsAppSDK.AI /
.MachineLearning to exact versions
Switching from "2.0.*" to exact pins (2.0.185 / 2.0.300) eliminates an
intermittent restore-time conflict where the floating versions could
transitively pull in Foundation 2.0.21 / InteractiveExperiences 2.0.13,
mismatching Microsoft.WindowsAppSDK 2.0.1's expected 2.0.20 / 2.0.12.
Bump deliberately when WindowsAppSDK itself is upgraded.
Co-Authored-By: Claude Opus 4.7
---
XrayUI-dev.csproj | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/XrayUI-dev.csproj b/XrayUI-dev.csproj
index d281658..cfee0cb 100644
--- a/XrayUI-dev.csproj
+++ b/XrayUI-dev.csproj
@@ -76,11 +76,14 @@
strip their runtime assets (DirectML.dll / onnxruntime.dll / Microsoft.Windows.AI.MachineLearning.dll).
IncludeAssets=compile keeps the reference assemblies for compile-time use; PrivateAssets=all
prevents them from flowing to consumers. -->
-
+
+
all
compile
-
+
all
compile
From c0d7ac4b61437849a5fa4f88df29de0c4e5974bd Mon Sep 17 00:00:00 2001
From: Zero <1270128439@qq.com>
Date: Sat, 23 May 2026 14:42:25 +0800
Subject: [PATCH 3/6] refactor(ui): decouple routing / proxy-mode business
codes from display strings
Mode state and persistence now use stable codes; display goes through a
derived *Text getter. Same UI behavior, but the data flow stops piggybacking
on translated Chinese strings -- which broke the moment i18n is introduced.
- ControlPanelViewModel.RoutingMode is now "smart" | "global" (matches
AppSettings.RoutingMode exactly, so the persistence layer is the identity
function). Display goes through RoutingModeText.
- SetProxyMode takes "system" | "manual" instead of the localized labels.
- XAML RadioMenuFlyoutItem.CommandParameter switched to the business codes;
the status-bar and mini-view TextBlocks now bind RoutingModeText.
- Drops the noise three-way in MainViewModel.InitializeAsync that mapped
business codes to business codes through Chinese labels.
The Chinese in RoutingModeText / MenuFlyoutItem.Text stays for now -- it's
display text, not a code. String extraction lands in the next commit.
Co-Authored-By: Claude Opus 4.7
---
ViewModels/ControlPanelViewModel.cs | 24 ++++++++++++++++++------
ViewModels/MainViewModel.cs | 4 ++--
Views/ControlPanelControl.xaml | 10 +++++-----
3 files changed, 25 insertions(+), 13 deletions(-)
diff --git a/ViewModels/ControlPanelViewModel.cs b/ViewModels/ControlPanelViewModel.cs
index d571661..fe8ab18 100644
--- a/ViewModels/ControlPanelViewModel.cs
+++ b/ViewModels/ControlPanelViewModel.cs
@@ -25,7 +25,7 @@ public partial class ControlPanelViewModel : ObservableObject
private bool _isRunning;
private bool _isTunMode;
private int _localPort = 16890;
- private string _routingMode = "智能分流";
+ private string _routingMode = "smart";
private bool _isSystemProxyEnabled = true;
private bool _isStartupEnabled;
private bool _isAutoConnect;
@@ -230,7 +230,7 @@ private async Task StartSelectedServerAsync()
var appSettings = await _settings.LoadSettingsAsync();
appSettings.LocalMixedPort = LocalPort;
- appSettings.RoutingMode = RoutingMode == "智能分流" ? "smart" : "global";
+ appSettings.RoutingMode = RoutingMode;
appSettings.IsTunMode = IsTunMode;
if (IsAutoConnect)
appSettings.LastAutoConnectServerId = server.Id;
@@ -318,7 +318,7 @@ public async Task ReapplyRoutingAsync()
{
var settings = await _settings.LoadSettingsAsync();
settings.LocalMixedPort = LocalPort;
- settings.RoutingMode = RoutingMode == "智能分流" ? "smart" : "global";
+ settings.RoutingMode = RoutingMode;
settings.IsTunMode = IsTunMode;
settings.IsSystemProxyEnabled = _isSystemProxyEnabled;
@@ -706,12 +706,22 @@ private async Task ShowDnsSettings()
// ── Routing mode ──────────────────────────────────────────────────────
+ /// Business code: "smart" | "global". This is what gets persisted to
+ /// settings.json and what XAML RadioButton.CommandParameter values match against.
+ /// For display, bind to .
public string RoutingMode
{
get => _routingMode;
- set => SetProperty(ref _routingMode, value);
+ set
+ {
+ if (SetProperty(ref _routingMode, value))
+ OnPropertyChanged(nameof(RoutingModeText));
+ }
}
+ /// Localized display string for the status bar / mini view.
+ public string RoutingModeText => _routingMode == "global" ? "全局路由" : "智能分流";
+
[RelayCommand]
private async Task SetRoutingMode(string mode)
{
@@ -721,7 +731,7 @@ private async Task SetRoutingMode(string mode)
RoutingMode = mode;
var s = await _settings.LoadSettingsAsync();
- s.RoutingMode = mode == "智能分流" ? "smart" : "global";
+ s.RoutingMode = mode;
await TrySaveSettingsAsync(s, "persist routing mode");
// Apply live if xray is currently running (UI only allows this when !IsTunMode).
@@ -753,7 +763,9 @@ public bool IsSystemProxyEnabled
[RelayCommand]
private async Task SetProxyMode(string mode)
{
- var want = mode == "全局代理";
+ // Business code: "system" = take over WinINet system proxy, "manual" = leave
+ // registry alone (user wires their apps to the local SOCKS port themselves).
+ var want = mode == "system";
// No-op guard: clicking the already-selected radio must not re-hit
// the registry or re-write settings.
diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs
index bfddeba..f4c66fa 100644
--- a/ViewModels/MainViewModel.cs
+++ b/ViewModels/MainViewModel.cs
@@ -46,7 +46,7 @@ public bool IsMiniMode
public string ActiveServerName =>
(ControlPanel.IsRunning ? _activeServer : ServerList.SelectedServer)?.Name ?? "未选择";
- public string MiniRoutingMode => ControlPanel.RoutingMode;
+ public string MiniRoutingMode => ControlPanel.RoutingModeText;
public IAsyncRelayCommand MiniStartStopCommand => ControlPanel.StartStopCommand;
public bool MiniIsRunning => ControlPanel.IsRunning;
public string MiniStatusText => ControlPanel.IsRunning ? _activeLatencyText : "未连接";
@@ -115,7 +115,7 @@ public async Task InitializeAsync(bool isBootLaunch = false)
// Load settings and apply to ControlPanel
var s = await _settings.LoadSettingsAsync();
ControlPanel.LocalPort = s.LocalMixedPort;
- ControlPanel.RoutingMode = s.RoutingMode == "global" ? "全局路由" : "智能分流";
+ ControlPanel.RoutingMode = s.RoutingMode;
ControlPanel.IsSystemProxyEnabled = s.IsSystemProxyEnabled;
ControlPanel.InitializePersonalize(s);
Personalize.LoadDisplayOptions(s);
diff --git a/Views/ControlPanelControl.xaml b/Views/ControlPanelControl.xaml
index e754089..0597451 100644
--- a/Views/ControlPanelControl.xaml
+++ b/Views/ControlPanelControl.xaml
@@ -61,13 +61,13 @@
IsChecked="{x:Bind ViewModel.IsGlobalProxyChecked, Mode=OneWay}"
IsEnabled="{x:Bind ViewModel.IsModeToggleEnabled, Mode=OneWay}"
Command="{x:Bind ViewModel.SetProxyModeCommand}"
- CommandParameter="全局代理" />
+ CommandParameter="system" />
+ CommandParameter="manual" />
+ CommandParameter="global" />
+ CommandParameter="smart" />
-
From 035a39d28fa6554e71344f988fa2466d0edba123 Mon Sep 17 00:00:00 2001
From: Zero <1270128439@qq.com>
Date: Sat, 23 May 2026 14:43:23 +0800
Subject: [PATCH 4/6] feat(i18n): localize dialogs, control panel, and main
viewmodel
Wires the runtime localization from commit 1 through the highest-traffic
surfaces. ServerList, ServerDetail, MainWindow chrome and assorted minor
dialogs are still hardcoded Chinese and land in a follow-up.
- L.cs grows into a strongly-typed accessor for ~80 keys grouped by surface
(dialog buttons, edit-server fields, error / startup / update / DNS /
chain proxy / preset-replace dialogs, control panel state).
- DialogService: every dialog now reads from L.* / Loc.Format. The
ShowConfirmationAsync default args (which can't be properties) switch
to nullable + ??= fallback inside the method body; IDialogService matches.
- ControlPanelViewModel state strings (Start/Stop button, StatusText,
RoutingModeText, error and update flows) go through L.*; UpdateMenuText
uses Loc.Format. MainViewModel.ActiveServerName / MiniStatusText too.
- ControlPanelControl.xaml gains x:Uid on every MenuFlyoutItem / Routing /
TUN label so PRI injects the localized .Text at build time; the original
Chinese stays as design-time fallback.
- resw files: ~280 keys. New ChainProxy / DNS / preset-replace keys cover
surfaces the original i18n stash never touched.
Debug.WriteLine and internal-only strings stay Chinese on purpose --
they're developer-facing.
Co-Authored-By: Claude Opus 4.7
---
Helpers/L.cs | 116 +++-
Services/DialogService.cs | 154 ++---
Services/IDialogService.cs | 2 +-
Strings/en-US/Resources.resw | 909 +++++++++++++++++++++++++++-
Strings/zh-CN/Resources.resw | 429 +++++++++++--
ViewModels/ControlPanelViewModel.cs | 32 +-
ViewModels/MainViewModel.cs | 5 +-
ViewModels/PersonalizeViewModel.cs | 8 +-
Views/ControlPanelControl.xaml | 39 +-
9 files changed, 1536 insertions(+), 158 deletions(-)
diff --git a/Helpers/L.cs b/Helpers/L.cs
index bfbced9..14d7b02 100644
--- a/Helpers/L.cs
+++ b/Helpers/L.cs
@@ -4,11 +4,121 @@ 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).
+/// (the resw entry key for XAML carries a property suffix like .Text;
+/// the C# key here matches the bare name).
///
-/// Entries are added incrementally as call sites are localized. Empty for now —
-/// see commit 3 of the i18n series.
+/// Entries are added incrementally as call sites are localized.
///
public static class L
{
+ // ── Generic dialog buttons ─────────────────────────────────────────────
+ public static string Dialog_OK => Loc.GetString("Dialog_OK");
+ public static string Dialog_Cancel => Loc.GetString("Dialog_Cancel");
+ public static string Dialog_Save => Loc.GetString("Dialog_Save");
+ public static string Dialog_Done => Loc.GetString("Dialog_Done");
+ public static string Dialog_Confirm => Loc.GetString("Dialog_Confirm");
+ public static string Dialog_Add => Loc.GetString("Dialog_Add");
+ public static string Dialog_Delete => Loc.GetString("Dialog_Delete");
+ public static string Dialog_Replace => Loc.GetString("Dialog_Replace");
+ public static string Dialog_Preparing => Loc.GetString("Dialog_Preparing");
+ public static string Dialog_On => Loc.GetString("Dialog_On");
+ public static string Dialog_Off => Loc.GetString("Dialog_Off");
+
+ // ── Confirmation dialogs ───────────────────────────────────────────────
+ public static string Confirm_ReplaceTitle => Loc.GetString("Confirm_ReplaceTitle");
+ public static string Confirm_ReplaceMsg => Loc.GetString("Confirm_ReplaceMsg");
+
+ // ── Import link dialog ─────────────────────────────────────────────────
+ public static string Import_Title => Loc.GetString("Import_Title");
+ public static string Import_Placeholder => Loc.GetString("Import_Placeholder");
+ public static string Import_SupportHint => Loc.GetString("Import_SupportHint");
+
+ // ── Edit server dialog ─────────────────────────────────────────────────
+ public static string EditServer_AddTitle => Loc.GetString("EditServer_AddTitle");
+ public static string EditServer_EditTitle => Loc.GetString("EditServer_EditTitle");
+ public static string EditServer_Name => Loc.GetString("EditServer_Name");
+ public static string EditServer_Address => Loc.GetString("EditServer_Address");
+ public static string EditServer_Port => Loc.GetString("EditServer_Port");
+ public static string EditServer_Protocol => Loc.GetString("EditServer_Protocol");
+ public static string EditServer_Encryption => Loc.GetString("EditServer_Encryption");
+ public static string EditServer_Password => Loc.GetString("EditServer_Password");
+ public static string EditServer_Transport => Loc.GetString("EditServer_Transport");
+ public static string EditServer_Path => Loc.GetString("EditServer_Path");
+ public static string EditServer_WsHost => Loc.GetString("EditServer_WsHost");
+ public static string EditServer_Security => Loc.GetString("EditServer_Security");
+ public static string EditServer_Fingerprint => Loc.GetString("EditServer_Fingerprint");
+ public static string EditServer_AllowInsecure => Loc.GetString("EditServer_AllowInsecure");
+ public static string EditServer_FlowPlaceholder => Loc.GetString("EditServer_FlowPlaceholder");
+
+ // ── Edit port dialog ───────────────────────────────────────────────────
+ public static string EditPort_Title => Loc.GetString("EditPort_Title");
+ public static string EditPort_Header => Loc.GetString("EditPort_Header");
+
+ // ── TUN mode ───────────────────────────────────────────────────────────
+ public static string Tun_EnableTitle => Loc.GetString("Tun_EnableTitle");
+
+ // ── Share link dialog ──────────────────────────────────────────────────
+ public static string Share_Title => Loc.GetString("Share_Title");
+ public static string Share_CopyLink => Loc.GetString("Share_CopyLink");
+
+ // ── Startup dialog ─────────────────────────────────────────────────────
+ public static string Startup_Title => Loc.GetString("Startup_Title");
+ public static string Startup_AutoStart => Loc.GetString("Startup_AutoStart");
+ public static string Startup_AutoConnect => Loc.GetString("Startup_AutoConnect");
+
+ // ── Edit server dialog (extras not in stash) ───────────────────────────
+ public static string EditServer_SocksUsername => Loc.GetString("EditServer_SocksUsername");
+ public static string EditServer_EchPlaceholder => Loc.GetString("EditServer_EchPlaceholder");
+ public static string EditServer_FinalmaskPlaceholder => Loc.GetString("EditServer_FinalmaskPlaceholder");
+
+ // ── Chain proxy dialog ─────────────────────────────────────────────────
+ public static string ChainProxy_AddTitle => Loc.GetString("ChainProxy_AddTitle");
+ public static string ChainProxy_EditTitle => Loc.GetString("ChainProxy_EditTitle");
+
+ // ── MainViewModel ──────────────────────────────────────────────────────
+ public static string Main_NoSelection => Loc.GetString("Main_NoSelection");
+ public static string Main_NotConnected => Loc.GetString("Main_NotConnected");
+
+ // ── ControlPanel ───────────────────────────────────────────────────────
+ public static string ControlPanel_Start => Loc.GetString("ControlPanel_Start");
+ public static string ControlPanel_Stop => Loc.GetString("ControlPanel_Stop");
+ public static string ControlPanel_StatusApplying => Loc.GetString("ControlPanel_StatusApplying");
+ public static string ControlPanel_StatusNotRunning => Loc.GetString("ControlPanel_StatusNotRunning");
+ public static string ControlPanel_RoutingGlobal => Loc.GetString("ControlPanel_RoutingGlobal");
+ public static string ControlPanel_RoutingSmart => Loc.GetString("ControlPanel_RoutingSmart");
+ public static string ControlPanel_UpdateFound => Loc.GetString("ControlPanel_UpdateFound");
+
+ // ── Error dialogs ──────────────────────────────────────────────────────
+ public static string Error_NoServer => Loc.GetString("Error_NoServer");
+ public static string Error_NoServerMsg => Loc.GetString("Error_NoServerMsg");
+ public static string Error_StartFailed => Loc.GetString("Error_StartFailed");
+ public static string Error_XrayStartFailed => Loc.GetString("Error_XrayStartFailed");
+ public static string Error_ReapplyFailed => Loc.GetString("Error_ReapplyFailed");
+ public static string Error_XrayReapplyFailed => Loc.GetString("Error_XrayReapplyFailed");
+ public static string Error_UpdateFailed => Loc.GetString("Error_UpdateFailed");
+ public static string Error_UpdaterLaunchFailed => Loc.GetString("Error_UpdaterLaunchFailed");
+
+ // ── Startup / Update / TUN ─────────────────────────────────────────────
+ public static string Startup_SetFailed => Loc.GetString("Startup_SetFailed");
+ public static string Update_Updating => Loc.GetString("Update_Updating");
+ public static string Tun_EnableMsg => Loc.GetString("Tun_EnableMsg");
+
+ // ── DNS settings dialog ────────────────────────────────────────────────
+ public static string Dns_DialogTitle => Loc.GetString("Dns_DialogTitle");
+ public static string Dns_ResetDefaults => Loc.GetString("Dns_ResetDefaults");
+ public static string Dns_ServerPlaceholder => Loc.GetString("Dns_ServerPlaceholder");
+ public static string Dns_QueryStrategyLabel => Loc.GetString("Dns_QueryStrategyLabel");
+ public static string Dns_EnableCacheLabel => Loc.GetString("Dns_EnableCacheLabel");
+ public static string Dns_DirectTitle => Loc.GetString("Dns_DirectTitle");
+ public static string Dns_DirectDesc => Loc.GetString("Dns_DirectDesc");
+ public static string Dns_ProxyTitle => Loc.GetString("Dns_ProxyTitle");
+ public static string Dns_ProxyDesc => Loc.GetString("Dns_ProxyDesc");
+ public static string Dns_TunOnlyHint => Loc.GetString("Dns_TunOnlyHint");
+ public static string Dns_Experimental => Loc.GetString("Dns_Experimental");
+ public static string Dns_Provider_Ali => Loc.GetString("Dns_Provider_Ali");
+ public static string Dns_Provider_Tencent => Loc.GetString("Dns_Provider_Tencent");
+ public static string Dns_Provider_Google => Loc.GetString("Dns_Provider_Google");
+ public static string Dns_Strategy_V4Only => Loc.GetString("Dns_Strategy_V4Only");
+ public static string Dns_Strategy_V6Only => Loc.GetString("Dns_Strategy_V6Only");
+ public static string Dns_Strategy_Auto => Loc.GetString("Dns_Strategy_Auto");
}
diff --git a/Services/DialogService.cs b/Services/DialogService.cs
index 6b99f8a..5375e77 100644
--- a/Services/DialogService.cs
+++ b/Services/DialogService.cs
@@ -31,7 +31,7 @@ public DialogService(Func xamlRootFactory)
{
var textBox = new TextBox
{
- PlaceholderText = "粘贴节点链接(支持多协议)",
+ PlaceholderText = L.Import_Placeholder,
AcceptsReturn = true,
Width = 360,
Height = 148,
@@ -41,9 +41,9 @@ public DialogService(Func xamlRootFactory)
};
var dialog = CreateDialog();
- dialog.Title = "导入节点链接";
- dialog.PrimaryButtonText = "确定";
- dialog.CloseButtonText = "取消";
+ dialog.Title = L.Import_Title;
+ dialog.PrimaryButtonText = L.Dialog_OK;
+ dialog.CloseButtonText = L.Dialog_Cancel;
dialog.DefaultButton = ContentDialogButton.Primary;
dialog.Content = new StackPanel
{
@@ -53,7 +53,7 @@ public DialogService(Func xamlRootFactory)
{
new TextBlock
{
- Text = "支持常见协议链接",
+ Text = L.Import_SupportHint,
Opacity = 0.65,
},
textBox
@@ -78,15 +78,15 @@ void SyncDialogButtons()
{
if (vm.IsAddPage)
{
- dialog.PrimaryButtonText = "添加";
- dialog.CloseButtonText = "取消";
+ dialog.PrimaryButtonText = L.Dialog_Add;
+ dialog.CloseButtonText = L.Dialog_Cancel;
dialog.DefaultButton = ContentDialogButton.Primary;
dialog.IsPrimaryButtonEnabled = vm.CanAddSubscription;
return;
}
dialog.PrimaryButtonText = string.Empty;
- dialog.CloseButtonText = "完成";
+ dialog.CloseButtonText = L.Dialog_Done;
dialog.DefaultButton = ContentDialogButton.Close;
dialog.IsPrimaryButtonEnabled = false;
}
@@ -111,19 +111,19 @@ void SyncDialogButtons()
public async Task ShowEditServerDialogAsync(ServerEntry? existing)
{
// ── Controls ──────────────────────────────────────────────────────
- var txtName = new TextBox { Header = "名称", Text = existing?.Name ?? string.Empty, MinWidth = 420 };
- var txtHost = new TextBox { Header = "地址 / 域名", Text = existing?.Host ?? string.Empty };
+ var txtName = new TextBox { Header = L.EditServer_Name, Text = existing?.Name ?? string.Empty, MinWidth = 420 };
+ var txtHost = new TextBox { Header = L.EditServer_Address, Text = existing?.Host ?? string.Empty };
var numPort = new NumberBox
{
- Header = "端口", Value = existing?.Port ?? 443, Minimum = 1, Maximum = 65535,
+ Header = L.EditServer_Port, Value = existing?.Port ?? 443, Minimum = 1, Maximum = 65535,
SpinButtonPlacementMode = NumberBoxSpinButtonPlacementMode.Inline
};
- var cmbProtocol = new ComboBox { Header = "协议", MinWidth = 200 };
+ var cmbProtocol = new ComboBox { Header = L.EditServer_Protocol, MinWidth = 200 };
foreach (var p in new[] { "ss", "vmess", "vless", "hysteria2", "trojan", "socks" })
cmbProtocol.Items.Add(p);
cmbProtocol.SelectedItem = existing?.Protocol?.ToLower() ?? "ss";
- var cmbEncryption = new ComboBox { Header = "加密方式 (SS)", MinWidth = 200 };
+ var cmbEncryption = new ComboBox { Header = L.EditServer_Encryption, MinWidth = 200 };
foreach (var m in new[]
{
"aes-128-gcm", "aes-256-gcm", "chacha20-ietf-poly1305", "2022-blake3-aes-128-gcm",
@@ -133,31 +133,31 @@ void SyncDialogButtons()
if (existing?.Encryption is { Length: > 0 } existingEnc && !cmbEncryption.Items.Contains(existingEnc))
cmbEncryption.Items.Add(existingEnc);
cmbEncryption.SelectedItem = existing?.Encryption ?? "aes-128-gcm";
- var txtUsername = new TextBox { Header = "用户名 (SOCKS)", Text = existing?.Username ?? string.Empty };
- var txtPassword = new PasswordBox { Header = "密码", Password = existing?.Password ?? string.Empty };
+ var txtUsername = new TextBox { Header = L.EditServer_SocksUsername, Text = existing?.Username ?? string.Empty };
+ var txtPassword = new PasswordBox { Header = L.EditServer_Password, Password = existing?.Password ?? string.Empty };
var txtUuid = new TextBox { Header = "UUID (VMess / VLESS)", Text = existing?.Uuid ?? string.Empty };
var numAlterId = new NumberBox
{ Header = "AlterId (VMess)", Value = existing?.AlterId ?? 0, Minimum = 0, Maximum = 65535 };
- var cmbNetwork = new ComboBox { Header = "传输协议", MinWidth = 200 };
+ var cmbNetwork = new ComboBox { Header = L.EditServer_Transport, MinWidth = 200 };
foreach (var n in new[] { "tcp", "ws", "grpc", "xhttp" })
cmbNetwork.Items.Add(n);
cmbNetwork.SelectedItem = existing?.Network ?? "tcp";
- var txtPath = new TextBox { Header = "路径 (WS/gRPC/XHTTP)", Text = existing?.Path ?? string.Empty };
- var txtWsHost = new TextBox { Header = "Host 头 (WS/XHTTP)", Text = existing?.WsHost ?? string.Empty };
- var cmbSecurity = new ComboBox { Header = "安全", MinWidth = 200 };
+ var txtPath = new TextBox { Header = L.EditServer_Path, Text = existing?.Path ?? string.Empty };
+ var txtWsHost = new TextBox { Header = L.EditServer_WsHost, Text = existing?.WsHost ?? string.Empty };
+ var cmbSecurity = new ComboBox { Header = L.EditServer_Security, MinWidth = 200 };
foreach (var s in new[] { "none", "tls", "reality" })
cmbSecurity.Items.Add(s);
cmbSecurity.SelectedItem = existing?.Security ?? "none";
var txtSni = new TextBox { Header = "SNI", Text = existing?.Sni ?? string.Empty };
- var txtFp = new TextBox { Header = "指纹 (uTLS)", Text = existing?.Fingerprint ?? string.Empty };
+ var txtFp = new TextBox { Header = L.EditServer_Fingerprint, Text = existing?.Fingerprint ?? string.Empty };
var chkAllowInsecure = new CheckBox
- { Content = "允许不安全连接(跳过证书校验)", IsChecked = existing?.AllowInsecure ?? false };
+ { Content = L.EditServer_AllowInsecure, IsChecked = existing?.AllowInsecure ?? false };
var txtEchConfigList = new TextBox
{
Header = "ECH ConfigList",
- PlaceholderText = "例如 udp://1.1.1.1 或服务端生成的 ECHConfig",
+ PlaceholderText = L.EditServer_EchPlaceholder,
Text = existing?.EchConfigList ?? string.Empty,
TextWrapping = TextWrapping.Wrap
};
@@ -173,12 +173,12 @@ void SyncDialogButtons()
var txtSpx = new TextBox { Header = "SpiderX (Reality)", Text = existing?.SpiderX ?? string.Empty };
var txtFlow = new TextBox
{
- Header = "Flow (VLESS)", PlaceholderText = "xtls-rprx-vision 或留空", Text = existing?.Flow ?? string.Empty
+ Header = "Flow (VLESS)", PlaceholderText = L.EditServer_FlowPlaceholder, Text = existing?.Flow ?? string.Empty
};
var txtVlessEncryption = new TextBox
{
Header = "VLESS encryption (PQ)",
- PlaceholderText = "留空 = none;或 mlkem768x25519plus.native.0rtt....",
+ PlaceholderText = L.EditServer_FinalmaskPlaceholder,
Text = existing?.VlessEncryption ?? string.Empty,
TextWrapping = TextWrapping.Wrap
};
@@ -294,9 +294,9 @@ void UpdateVisibility()
};
var dialog = CreateDialog();
- dialog.Title = existing == null ? "手动添加服务器" : "编辑服务器";
- dialog.PrimaryButtonText = "保存";
- dialog.CloseButtonText = "取消";
+ dialog.Title = existing == null ? L.EditServer_AddTitle : L.EditServer_EditTitle;
+ dialog.PrimaryButtonText = L.Dialog_Save;
+ dialog.CloseButtonText = L.Dialog_Cancel;
dialog.DefaultButton = ContentDialogButton.Primary;
dialog.Content = scrollViewer;
@@ -385,9 +385,9 @@ void UpdateVisibility()
ServerEntry? saved = null;
var dialog = CreateDialog();
- dialog.Title = existing is null ? "链式代理" : "编辑链式代理";
- dialog.PrimaryButtonText = "保存";
- dialog.CloseButtonText = "取消";
+ dialog.Title = existing is null ? L.ChainProxy_AddTitle : L.ChainProxy_EditTitle;
+ dialog.PrimaryButtonText = L.Dialog_Save;
+ dialog.CloseButtonText = L.Dialog_Cancel;
dialog.DefaultButton = ContentDialogButton.Primary;
dialog.Content = content;
@@ -409,7 +409,7 @@ void UpdateVisibility()
{
var numBox = new NumberBox
{
- Header = "本地端口",
+ Header = L.EditPort_Header,
Value = currentPort,
Minimum = 1024,
Maximum = 65535,
@@ -417,9 +417,9 @@ void UpdateVisibility()
};
var dialog = CreateDialog();
- dialog.Title = "编辑本地端口";
- dialog.PrimaryButtonText = "确定";
- dialog.CloseButtonText = "取消";
+ dialog.Title = L.EditPort_Title;
+ dialog.PrimaryButtonText = L.Dialog_OK;
+ dialog.CloseButtonText = L.Dialog_Cancel;
dialog.DefaultButton = ContentDialogButton.Primary;
dialog.Content = new StackPanel
{
@@ -430,7 +430,7 @@ void UpdateVisibility()
numBox,
new TextBlock
{
- Text = $"有效范围:{numBox.Minimum} - {numBox.Maximum}",
+ Text = Loc.Format("EditPort_Range", numBox.Minimum, numBox.Maximum),
Opacity = 0.65,
}
}
@@ -444,9 +444,11 @@ void UpdateVisibility()
// ── Error ─────────────────────────────────────────────────────────────
- public async Task ShowConfirmationAsync(string title, string message, string confirmText = "确定",
- string cancelText = "取消", bool isDanger = false)
+ public async Task ShowConfirmationAsync(string title, string message, string? confirmText = null,
+ string? cancelText = null, bool isDanger = false)
{
+ confirmText ??= L.Dialog_OK;
+ cancelText ??= L.Dialog_Cancel;
var content = new TextBlock
{
Text = message,
@@ -474,10 +476,10 @@ public async Task ShowTunConfirmationDialogAsync(AppSettings settings)
var content = new TunConfirmationDialog(settings.TunMtu, settings.TunOutboundInterface);
var dialog = CreateDialog();
- dialog.Title = "开启TUN模式";
+ dialog.Title = L.Tun_EnableTitle;
dialog.Content = content;
- dialog.PrimaryButtonText = "确认";
- dialog.CloseButtonText = "取消";
+ dialog.PrimaryButtonText = L.Dialog_Confirm;
+ dialog.CloseButtonText = L.Dialog_Cancel;
dialog.DefaultButton = ContentDialogButton.Primary;
if (await dialog.ShowAsync() != ContentDialogResult.Primary)
@@ -493,7 +495,7 @@ public async Task ShowErrorAsync(string title, string message, XamlRoot? xamlRoo
var dialog = CreateDialog(xamlRoot);
dialog.Title = title;
dialog.Content = message;
- dialog.CloseButtonText = "确定";
+ dialog.CloseButtonText = L.Dialog_OK;
await dialog.ShowAsync();
}
@@ -506,7 +508,7 @@ public async Task ShowProgressDialogAsync(string title, Func,
var statusText = new TextBlock
{
- Text = "正在准备…",
+ Text = L.Dialog_Preparing,
TextWrapping = TextWrapping.Wrap,
MaxWidth = 320,
HorizontalAlignment = HorizontalAlignment.Center,
@@ -521,7 +523,7 @@ public async Task ShowProgressDialogAsync(string title, Func,
var dialog = CreateDialog(xamlRoot);
dialog.Title = title;
- dialog.CloseButtonText = "取消";
+ dialog.CloseButtonText = L.Dialog_Cancel;
dialog.Content = new StackPanel
{
Spacing = 16,
@@ -602,7 +604,7 @@ public async Task ShowProgressBarDialogAsync(string title,
var statusText = new TextBlock
{
- Text = "正在准备…",
+ Text = L.Dialog_Preparing,
TextWrapping = TextWrapping.Wrap,
MaxWidth = 320,
HorizontalAlignment = HorizontalAlignment.Center,
@@ -618,7 +620,7 @@ public async Task ShowProgressBarDialogAsync(string title,
var dialog = CreateDialog(xamlRoot);
dialog.Title = title;
- dialog.CloseButtonText = "取消";
+ dialog.CloseButtonText = L.Dialog_Cancel;
dialog.Content = new StackPanel
{
Spacing = 12,
@@ -729,7 +731,7 @@ public async Task ShowShareLinkDialogAsync(string serverName, string link)
header.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
var titleText = new TextBlock
{
- Text = "分享节点",
+ Text = L.Share_Title,
FontSize = 20,
FontWeight = Microsoft.UI.Text.FontWeights.SemiBold,
VerticalAlignment = VerticalAlignment.Center,
@@ -762,7 +764,7 @@ public async Task ShowShareLinkDialogAsync(string serverName, string link)
};
if (Application.Current.Resources.TryGetValue("SubtleButtonStyle", out var subtleStyle2))
nameCopyBtn.Style = (Style)subtleStyle2;
- ToolTipService.SetToolTip(nameCopyBtn, "复制链接");
+ ToolTipService.SetToolTip(nameCopyBtn, L.Share_CopyLink);
nameCopyBtn.Click += async (_, _) =>
{
@@ -815,15 +817,15 @@ public async Task ShowShareLinkDialogAsync(string serverName, string link)
var toggle = new ToggleSwitch
{
IsOn = currentEnabled,
- OnContent = "开",
- OffContent = "关",
+ OnContent = L.Dialog_On,
+ OffContent = L.Dialog_Off,
MinWidth = 0,
Margin = new Thickness(0),
};
var toggleLabel = new TextBlock
{
- Text = "开机自动启动",
+ Text = L.Startup_AutoStart,
VerticalAlignment = VerticalAlignment.Center,
};
@@ -837,7 +839,7 @@ public async Task ShowShareLinkDialogAsync(string serverName, string link)
var checkBox = new CheckBox
{
- Content = "自动连接上次节点",
+ Content = L.Startup_AutoConnect,
IsChecked = currentAutoConnect,
IsEnabled = currentEnabled,
Margin = new Thickness(16, 0, 0, 0),
@@ -846,9 +848,9 @@ public async Task ShowShareLinkDialogAsync(string serverName, string link)
toggle.Toggled += (_, _) => checkBox.IsEnabled = toggle.IsOn;
var dialog = CreateDialog();
- dialog.Title = "开机启动";
- dialog.PrimaryButtonText = "确认";
- dialog.CloseButtonText = "取消";
+ dialog.Title = L.Startup_Title;
+ dialog.PrimaryButtonText = L.Dialog_Confirm;
+ dialog.CloseButtonText = L.Dialog_Cancel;
dialog.DefaultButton = ContentDialogButton.Primary;
dialog.Content = new StackPanel
{
@@ -870,30 +872,30 @@ public async Task ShowDnsSettingsDialogAsync(AppSettings settings, bool is
var directBox = new TextBox
{
Text = settings.DirectDnsServer ?? string.Empty,
- PlaceholderText = "输入 IP 或 DoH 地址 (留空为自动)",
+ PlaceholderText = L.Dns_ServerPlaceholder,
HorizontalAlignment = HorizontalAlignment.Stretch,
};
var proxyBox = new TextBox
{
Text = settings.ProxyDnsServer ?? string.Empty,
- PlaceholderText = "输入 IP 或 DoH 地址 (留空为自动)",
+ PlaceholderText = L.Dns_ServerPlaceholder,
HorizontalAlignment = HorizontalAlignment.Stretch,
};
var directPresets = CreatePresetButtons(directBox,
- ("阿里", "223.5.5.5"),
- ("腾讯", "119.29.29.29"),
+ (L.Dns_Provider_Ali, "223.5.5.5"),
+ (L.Dns_Provider_Tencent, "119.29.29.29"),
("114", "114.114.114.114"),
("DoH", "https://dns.alidns.com/dns-query"));
var proxyPresets = CreatePresetButtons(proxyBox,
- ("谷歌", "8.8.8.8"),
+ (L.Dns_Provider_Google, "8.8.8.8"),
("CF", "1.1.1.1"),
("Quad9", "9.9.9.9"),
("DoH", "https://cloudflare-dns.com/dns-query"));
var strategyCmb = new ComboBox { MinWidth = 100 };
- foreach (var item in new[] { "仅 IPv4", "仅 IPv6", "自动" })
+ foreach (var item in new[] { L.Dns_Strategy_V4Only, L.Dns_Strategy_V6Only, L.Dns_Strategy_Auto })
strategyCmb.Items.Add(item);
strategyCmb.SelectedIndex = settings.DnsQueryStrategy switch
{
@@ -905,8 +907,8 @@ public async Task ShowDnsSettingsDialogAsync(AppSettings settings, bool is
var cacheSwitch = new ToggleSwitch
{
IsOn = settings.DnsCacheEnabled,
- OnContent = "开",
- OffContent = "关",
+ OnContent = L.Dialog_On,
+ OffContent = L.Dialog_Off,
MinWidth = 0,
Margin = new Thickness(0),
};
@@ -915,8 +917,8 @@ public async Task ShowDnsSettingsDialogAsync(AppSettings settings, bool is
{
IsOn = settings.FakeDnsEnabled && isTunMode,
IsEnabled = isTunMode,
- OnContent = "开",
- OffContent = "关",
+ OnContent = L.Dialog_On,
+ OffContent = L.Dialog_Off,
MinWidth = 0,
Margin = new Thickness(0),
};
@@ -926,7 +928,7 @@ public async Task ShowDnsSettingsDialogAsync(AppSettings settings, bool is
Text = "FakeDNS",
VerticalAlignment = VerticalAlignment.Center,
};
- ToolTipService.SetToolTip(fakeDnsTitleText, "仅TUN模式有效");
+ ToolTipService.SetToolTip(fakeDnsTitleText, L.Dns_TunOnlyHint);
var fakeDnsLabel = new StackPanel
{
@@ -938,7 +940,7 @@ public async Task ShowDnsSettingsDialogAsync(AppSettings settings, bool is
fakeDnsTitleText,
new TextBlock
{
- Text = "实验性",
+ Text = L.Dns_Experimental,
FontSize = 10,
VerticalAlignment = VerticalAlignment.Center,
Foreground =
@@ -956,8 +958,8 @@ public async Task ShowDnsSettingsDialogAsync(AppSettings settings, bool is
fakeDnsRow.Children.Add(fakeDnsLabel);
fakeDnsRow.Children.Add(fakeDnsSwitch);
- var strategyRow = CreateLabelRow("查询策略", strategyCmb);
- var cacheRow = CreateLabelRow("启用 DNS 缓存", cacheSwitch);
+ var strategyRow = CreateLabelRow(L.Dns_QueryStrategyLabel, strategyCmb);
+ var cacheRow = CreateLabelRow(L.Dns_EnableCacheLabel, cacheSwitch);
var content = new StackPanel
{
@@ -970,10 +972,10 @@ public async Task ShowDnsSettingsDialogAsync(AppSettings settings, bool is
Spacing = 6,
Children =
{
- new TextBlock { Text = "直连 DNS", FontWeight = Microsoft.UI.Text.FontWeights.SemiBold },
+ new TextBlock { Text = L.Dns_DirectTitle, FontWeight = Microsoft.UI.Text.FontWeights.SemiBold },
new TextBlock
{
- Text = "用于国内域名 (geosite:cn),走直连出站解析",
+ Text = L.Dns_DirectDesc,
FontSize = 11,
Opacity = 0.6,
TextWrapping = TextWrapping.Wrap,
@@ -988,10 +990,10 @@ public async Task ShowDnsSettingsDialogAsync(AppSettings settings, bool is
Spacing = 6,
Children =
{
- new TextBlock { Text = "代理 DNS", FontWeight = Microsoft.UI.Text.FontWeights.SemiBold },
+ new TextBlock { Text = L.Dns_ProxyTitle, FontWeight = Microsoft.UI.Text.FontWeights.SemiBold },
new TextBlock
{
- Text = "用于境外域名,经代理出站解析,防止 DNS 污染",
+ Text = L.Dns_ProxyDesc,
FontSize = 11,
Opacity = 0.6,
TextWrapping = TextWrapping.Wrap,
@@ -1006,10 +1008,10 @@ public async Task ShowDnsSettingsDialogAsync(AppSettings settings, bool is
};
var dialog = CreateDialog();
- dialog.Title = "DNS 设置";
- dialog.PrimaryButtonText = "保存";
- dialog.SecondaryButtonText = "重置默认";
- dialog.CloseButtonText = "取消";
+ dialog.Title = L.Dns_DialogTitle;
+ dialog.PrimaryButtonText = L.Dialog_Save;
+ dialog.SecondaryButtonText = L.Dns_ResetDefaults;
+ dialog.CloseButtonText = L.Dialog_Cancel;
dialog.DefaultButton = ContentDialogButton.Primary;
dialog.Content = content;
diff --git a/Services/IDialogService.cs b/Services/IDialogService.cs
index b646180..fafaf21 100644
--- a/Services/IDialogService.cs
+++ b/Services/IDialogService.cs
@@ -14,7 +14,7 @@ public interface IDialogService
Task ShowChainProxyDialogAsync(IEnumerable servers, ServerEntry? existing = null);
Task ShowEditPortDialogAsync(int currentPort);
Task ShowErrorAsync(string title, string message, XamlRoot? xamlRoot = null);
- Task ShowConfirmationAsync(string title, string message, string confirmText = "确定", string cancelText = "取消", bool isDanger = false);
+ Task ShowConfirmationAsync(string title, string message, string? confirmText = null, string? cancelText = null, bool isDanger = false);
///
/// Shows the TUN confirmation dialog. Mutates .TunMtu and
/// .TunOutboundInterface in-place on confirm. Returns true if
diff --git a/Strings/en-US/Resources.resw b/Strings/en-US/Resources.resw
index 1feca4c..790f56c 100644
--- a/Strings/en-US/Resources.resw
+++ b/Strings/en-US/Resources.resw
@@ -1,5 +1,64 @@
-
+
+
@@ -58,8 +117,848 @@
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.
+
+ Proxy Console
-
+
+ Toggle mini window
+
+
+ Back to full window
+
+
+ Close
+
+
+ Open Window
+
+
+ Exit
+
+
+ Server List
+
+
+ Add
+
+
+ Edit
+
+
+ Delete
+
+
+ Share
+
+
+ Import Nodes
+
+
+ Add Subscription
+
+
+ Add Manually
+
+
+ Search servers
+
+
+ Filter:
+
+
+ Filter
+
+
+ Sort
+
+
+ Default
+
+
+ Active
+
+
+ Protocol
+
+
+ Only available when filter is "All Servers"
+
+
+ All Servers
+
+
+ Ungrouped
+
+
+ (Unnamed Subscription)
+
+
+ (Deleted Subscription)
+
+
+ ● Active
+
+
+ Start
+
+
+ Stop
+
+
+ Idle
+
+
+ Applying...
+
+
+ Personalize
+
+
+ GitHub
+
+
+ Edit Local Port
+
+
+ Log
+
+
+ Proxy Setting
+
+
+ Global Proxy
+
+
+ No Proxy Takeover
+
+
+ Startup
+
+
+ Routing Settings
+
+
+ Global
+
+
+ Smart
+
+
+ Custom Rules...
+
+
+ TUN Mode
+
+
+ Routing:
+
+
+ New version {0}
+
+
+ Server Details
+
+
+ Name
+
+
+ Protocol
+
+
+ Address
+
+
+ Port
+
+
+ Transport
+
+
+ Encryption
+
+
+ Security
+
+
+ AI Access Unlock
+
+
+ Open {0} in browser
+
+
+ Latency
+
+
+ Retest latency
+
+
+ Copy share link
+
+
+ No server selected
+
+
+ Not tested
+
+
+ Testing...
+
+
+ Timeout
+
+
+ Failed
+
+
+ Personalization
+
+
+ Theme
+
+
+ Choose the app appearance mode
+
+
+ Light
+
+
+ Dark
+
+
+ System
+
+
+ Background Effect
+
+
+ Choose window background material
+
+
+ Acrylic
+
+
+ Protocol Colors
+
+
+ Reset
+
+
+ Other Protocols
+
+
+ Unmatched protocols will use this color
+
+
+ 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
+
+
+ Add Subscription
+
+
+ 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
+
+
+ No valid nodes could be parsed from the subscription.
+
+
+ Please stop the proxy before refreshing
+
+
+ Update failed: {0}
+
+
+ Please stop the proxy before deleting
+
+
+ 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
+
+
+ Proxy Log
+
+
+ Not running
+
+
+ Running
+
+
+ ({0} lines)
+
+
+ Log privacy settings
+
+
+ IP Address Mask
+
+
+ Off
+
+
+ Auto Scroll
+
+
+ Copy All
+
+
+ Clear
+
+
+ Log Privacy Settings
+
+
+ Saved. Takes effect on next TUN session start.
+
+
+ Save failed: {0}
+
+
+ OK
+
+
+ Cancel
+
+
+ Save
+
+
+ Done
+
+
+ Confirm
+
+
+ Add
+
+
+ Delete
+
+
+ Replace
+
+
+ Preparing…
+
+
+ On
+
+
+ Off
+
+
+ Paste node links (multi-protocol supported)
+
+
+ Import Node Links
+
+
+ Supports common protocol links
+
+
+ Parse Failed
+
+
+ Could not identify valid node links. Please check and try again.
+
+
+ Add Server Manually
+
+
+ Edit Server
+
+
+ Name
+
+
+ Address / Domain
+
+
+ Port
+
+
+ Protocol
+
+
+ Encryption (SS)
+
+
+ Password
+
+
+ UUID (VMess / VLESS)
+
+
+ AlterId (VMess)
+
+
+ Transport Protocol
+
+
+ Path (WS/gRPC/XHTTP)
+
+
+ Host Header (WS/XHTTP)
+
+
+ Security
+
+
+ Fingerprint (uTLS)
+
+
+ Allow insecure connection (skip certificate verification)
+
+
+ xtls-rprx-vision or leave empty
+
+
+ Edit Local Port
+
+
+ Local Port
+
+
+ Valid range: {0} - {1}
+
+
+ Share Node
+
+
+ Copy Link
+
+
+ Sharing Not Supported
+
+
+ This node protocol does not support generating share links.
+
+
+ Startup
+
+
+ Launch at startup
+
+
+ Auto-connect to last node
+
+
+ Startup settings failed
+
+
+ Confirm Delete
+
+
+ Are you sure you want to delete {0}?
+
+
+ Replace current configuration?
+
+
+ This will overwrite all current servers and routing rules. We strongly recommend exporting a backup first. Continue?
+
+
+ Username (SOCKS)
+
+
+ e.g. udp://1.1.1.1 or server-generated ECHConfig
+
+
+ Empty = none; or mlkem768x25519plus.native.0rtt...
+
+
+ Chain proxy
+
+
+ Edit chain proxy
+
+
+ DNS Settings
+
+
+ DNS Settings
+
+
+ Reset to defaults
+
+
+ Enter IP or DoH address (empty = auto)
+
+
+ Query strategy
+
+
+ Enable DNS cache
+
+
+ Direct DNS
+
+
+ For domestic domains (geosite:cn), resolved via direct outbound
+
+
+ Proxy DNS
+
+
+ For foreign domains, resolved via proxy outbound to prevent DNS pollution
+
+
+ TUN mode only
+
+
+ Experimental
+
+
+ Alibaba
+
+
+ Tencent
+
+
+ Google
+
+
+ IPv4 only
+
+
+ IPv6 only
+
+
+ Auto
+
+
+ No Server Selected
+
+
+ Please select a server from the list first
+
+
+ Start Failed
+
+
+ xray failed to start. Please check server configuration.
+
+
+ Apply Configuration Failed
+
+
+ xray failed to apply new configuration and has stopped.
+
+
+ Update Failed
+
+
+ Could not launch updater: {0}
+
+
+ Updating XrayUI
+
+
+ Enable TUN Mode
+
+
+ Enabling TUN mode requires administrator privileges. The app will restart. Continue?
+
+
+ Not selected
+
+
+ Not connected
+
+
+ Copy to clipboard
+
+
+ Copied to clipboard
+
+
+ Updating routing data
+
+
+ Already Up to Date
+
+
+ geoip.dat and geosite.dat are already the latest version. No download needed.
+
+
+ Update Successful
+
+
+ Updated. Please restart manually in TUN mode for changes to take effect.
+
+
+ Updated and xray reloaded.
+
+
+ Data files updated, but xray restart failed: {0}
+
+
+ Updated. Please restart xray for changes to take effect.
+
+
+ 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…
+
+
+ Update checksum file format error
+
+
+ Could not download update checksum file:
+
+
+ Update verification failed: SHA256 does not match the server checksum.
+
+
+ Extracting update…
+
+
+ Update extraction failed:
+
+
+ Verifying update…
+
+
+ Update package is invalid: required files are missing.
+
+
+ Update version mismatch: expected {0}, got {1}.
+
+
+ Updater component missing. Please re-download the full installer.
+
+
+ Preparing to restart…
+
+
+ Downloading {0} … {1:0.0} / {2:0.0} MB
+
+
+ Downloading {0} … {1:0.0} MB
+
+
+ Edit Rule
+
+
+ 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
+
+
+ just now
+
+
+ {0} min ago
+
+
+ {0} hr ago
+
+
+ {0} days ago
+
+
+ Last updated: {0}
+
+
+ Cannot open registry key:
+
+
+ Local Port
+
+
+ Logs
+
+
+ Proxy Setting
+
+
+ Global Proxy
+
+
+ No Proxy Takeover
+
+
+ Startup
+
+
+ Routing
+
+
+ Global
+
+
+ Smart
+
+
+ Custom Rules...
+
+
+ TUN Mode
+
+
+ Routing:
+
+
+ Language
+
+
+ Choose the app display language
+
+
\ No newline at end of file
diff --git a/Strings/zh-CN/Resources.resw b/Strings/zh-CN/Resources.resw
index e7fa934..a441cd5 100644
--- a/Strings/zh-CN/Resources.resw
+++ b/Strings/zh-CN/Resources.resw
@@ -1,37 +1,18 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
@@ -46,20 +27,392 @@
-
- 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.
-
+ 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 模式
+ 路由:
+
+
+ 语言
+ 选择应用的显示语言
+
diff --git a/ViewModels/ControlPanelViewModel.cs b/ViewModels/ControlPanelViewModel.cs
index fe8ab18..bc8b252 100644
--- a/ViewModels/ControlPanelViewModel.cs
+++ b/ViewModels/ControlPanelViewModel.cs
@@ -20,7 +20,7 @@ public partial class ControlPanelViewModel : ObservableObject
private readonly IUpdateService _update;
private UpdateInfo? _availableUpdate;
private bool _isUpdateAvailable;
- private string _startStopButtonContent = "启动";
+ private string _startStopButtonContent = L.ControlPanel_Start;
private bool _startStopButtonChecked;
private bool _isRunning;
private bool _isTunMode;
@@ -131,13 +131,13 @@ public bool IsRunning
}
public string StatusText =>
- IsReapplying ? "正在应用..." :
+ IsReapplying ? L.ControlPanel_StatusApplying :
IsRunning ? _activeServerName :
- "未运行";
+ L.ControlPanel_StatusNotRunning;
private void OnIsRunningChanged(bool value)
{
- StartStopButtonContent = value ? "停止" : "启动";
+ StartStopButtonContent = value ? L.ControlPanel_Stop : L.ControlPanel_Start;
StartStopButtonChecked = value;
OnPropertyChanged(nameof(StatusText));
OnPropertyChanged(nameof(IsModeToggleEnabled));
@@ -224,7 +224,7 @@ private async Task StartSelectedServerAsync()
var server = GetSelectedServer();
if (server is null)
{
- await _dialogs.ShowErrorAsync("未选择服务器", "请先从列表中选择服务器");
+ await _dialogs.ShowErrorAsync(L.Error_NoServer, L.Error_NoServerMsg);
return false;
}
@@ -247,9 +247,9 @@ private async Task StartSelectedServerAsync()
if (!ok)
{
var detail = string.IsNullOrEmpty(_xray.LastError)
- ? "xray 启动失败. 请检查服务器配置."
+ ? L.Error_XrayStartFailed
: _xray.LastError;
- await _dialogs.ShowErrorAsync("启动失败", detail);
+ await _dialogs.ShowErrorAsync(L.Error_StartFailed, detail);
return false;
}
@@ -292,7 +292,7 @@ private async Task HandleStartStopFailureAsync(Exception ex)
_activeServer = null;
_activeServerName = string.Empty;
IsRunning = false;
- await _dialogs.ShowErrorAsync("启动失败", ex.Message);
+ await _dialogs.ShowErrorAsync(L.Error_StartFailed, ex.Message);
}
///
@@ -328,7 +328,7 @@ public async Task ReapplyRoutingAsync()
if (!ok)
{
var detail = string.IsNullOrEmpty(_xray.LastError)
- ? "xray 应用新配置失败,已停止。"
+ ? L.Error_XrayReapplyFailed
: _xray.LastError;
await HandleReapplyFailureAsync(detail);
return;
@@ -385,7 +385,7 @@ private async Task HandleReapplyFailureAsync(string detail)
_activeServerName = string.Empty;
IsRunning = false;
- await _dialogs.ShowErrorAsync("应用新配置失败", detail);
+ await _dialogs.ShowErrorAsync(L.Error_ReapplyFailed, detail);
}
@@ -720,7 +720,7 @@ public string RoutingMode
}
/// Localized display string for the status bar / mini view.
- public string RoutingModeText => _routingMode == "global" ? "全局路由" : "智能分流";
+ public string RoutingModeText => _routingMode == "global" ? L.ControlPanel_RoutingGlobal : L.ControlPanel_RoutingSmart;
[RelayCommand]
private async Task SetRoutingMode(string mode)
@@ -830,7 +830,7 @@ private async Task OpenStartupSettings()
}
catch (Exception ex)
{
- await _dialogs.ShowErrorAsync("开机启动设置失败", ex.Message);
+ await _dialogs.ShowErrorAsync(L.Startup_SetFailed, ex.Message);
return;
}
@@ -893,7 +893,7 @@ private set
}
public Visibility UpdateBadgeVisibility => _isUpdateAvailable ? Visibility.Visible : Visibility.Collapsed;
- public string UpdateMenuText => $"发现新版本 {_availableUpdate?.NewVersion}";
+ public string UpdateMenuText => Loc.Format("ControlPanel_UpdateFound", _availableUpdate?.NewVersion);
/// Called from MainViewModel after the background check completes.
/// Pass null to clear (e.g. after a failed update attempt).
@@ -916,7 +916,7 @@ private async Task UpdateAppAsync()
UpdateStaging? staging = null;
try
{
- await _dialogs.ShowProgressBarDialogAsync("正在更新 XrayUI",
+ await _dialogs.ShowProgressBarDialogAsync(L.Update_Updating,
async (progress, ct) =>
{
staging = await _update.DownloadVerifyAndExtractAsync(info, proxy, progress, ct);
@@ -929,7 +929,7 @@ await _dialogs.ShowProgressBarDialogAsync("正在更新 XrayUI",
}
catch (Exception ex)
{
- await _dialogs.ShowErrorAsync("更新失败", ex.Message);
+ await _dialogs.ShowErrorAsync(L.Error_UpdateFailed, ex.Message);
return;
}
@@ -944,7 +944,7 @@ await _dialogs.ShowProgressBarDialogAsync("正在更新 XrayUI",
}
catch (Exception ex)
{
- await _dialogs.ShowErrorAsync("更新失败", "无法启动升级器:" + ex.Message);
+ await _dialogs.ShowErrorAsync(L.Error_UpdateFailed, Loc.Format("Error_UpdaterLaunchFailed", ex.Message));
return;
}
diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs
index f4c66fa..d4fa947 100644
--- a/ViewModels/MainViewModel.cs
+++ b/ViewModels/MainViewModel.cs
@@ -2,6 +2,7 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using XrayUI.Helpers;
using XrayUI.Models;
using XrayUI.Services;
@@ -44,12 +45,12 @@ public bool IsMiniMode
}
public string ActiveServerName =>
- (ControlPanel.IsRunning ? _activeServer : ServerList.SelectedServer)?.Name ?? "未选择";
+ (ControlPanel.IsRunning ? _activeServer : ServerList.SelectedServer)?.Name ?? L.Main_NoSelection;
public string MiniRoutingMode => ControlPanel.RoutingModeText;
public IAsyncRelayCommand MiniStartStopCommand => ControlPanel.StartStopCommand;
public bool MiniIsRunning => ControlPanel.IsRunning;
- public string MiniStatusText => ControlPanel.IsRunning ? _activeLatencyText : "未连接";
+ public string MiniStatusText => ControlPanel.IsRunning ? _activeLatencyText : L.Main_NotConnected;
public Visibility MiniDotVisibility => ControlPanel.IsRunning ? Visibility.Visible : Visibility.Collapsed;
public MainViewModel(
diff --git a/ViewModels/PersonalizeViewModel.cs b/ViewModels/PersonalizeViewModel.cs
index e92feb7..a773b29 100644
--- a/ViewModels/PersonalizeViewModel.cs
+++ b/ViewModels/PersonalizeViewModel.cs
@@ -202,10 +202,10 @@ public Task ExportPresetAsync() =>
public async Task ConfirmAndImportPresetAsync()
{
var confirmed = await _dialogs.ShowConfirmationAsync(
- "替换当前配置?",
- "此操作将覆盖您当前的全部节点和路由规则,强烈建议先导出备份。是否继续?",
- "替换",
- "取消",
+ L.Confirm_ReplaceTitle,
+ L.Confirm_ReplaceMsg,
+ L.Dialog_Replace,
+ L.Dialog_Cancel,
isDanger: true);
if (!confirmed)
return null;
diff --git a/Views/ControlPanelControl.xaml b/Views/ControlPanelControl.xaml
index 0597451..9252a68 100644
--- a/Views/ControlPanelControl.xaml
+++ b/Views/ControlPanelControl.xaml
@@ -51,18 +51,23 @@
Command="{x:Bind ViewModel.UpdateAppCommand}"
Visibility="{x:Bind ViewModel.UpdateBadgeVisibility, Mode=OneWay}" />
-
-
-
-
+
-
-
-
-
+
-
-
-
@@ -157,7 +168,8 @@
Spacing="8"
VerticalAlignment="Center"
HorizontalAlignment="Center">
-
-
From b25fbfe7dbe13138825f8b5e357418ff07676bf3 Mon Sep 17 00:00:00 2001
From: Zero <1270128439@qq.com>
Date: Sat, 23 May 2026 18:43:26 +0800
Subject: [PATCH 5/6] feat(i18n): localize main views, windows, and remaining
dialogs
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Covers the second wave of i18n work — the main user surface and the
secondary dialogs. Static XAML labels move to x:Uid + .Text/.Content
resw entries; attached properties (AutomationProperties.Name,
ToolTipService.ToolTip) and Window.Title get wired up via code-behind
because x:Uid is awkward for those.
Files touched:
- ServerList / ServerDetail (controls + view models)
- MainWindow (title bar + tray menu)
- LogWindow (toolbar + status + privacy dialog)
- CustomRulesWindow + CustomRulesViewModel + AddRuleDialog
- PersonalizeControl remaining labels + import/export feedback
- AddChainProxyDialog, TunConfirmationDialog, ManageSubscriptionsDialog
- SubscriptionEntry.LastUpdatedText relative-time strings
- UpdateService progress + exception messages (surface in dialogs)
- ControlPanelControl Personalize tooltip
Out of scope (Chinese strings deliberately preserved):
- Debug.WriteLine / dev logs in XrayService / TunService /
SystemProxyService / ControlPanelViewModel
- LanguageHelper.SupportedLanguages DisplayName (endonym convention)
zh-CN/Resources.resw diff is inflated by the .NET ResX writer
reformatting compact entries to the standard multi-line shape;
content is preserved.
Co-Authored-By: Claude Opus 4.7
---
Helpers/L.cs | 142 +-
Helpers/LanguageHelper.cs | 25 +-
MainWindow.xaml | 15 +-
MainWindow.xaml.cs | 9 +-
Models/SubscriptionEntry.cs | 13 +-
Services/UpdateService.cs | 26 +-
Strings/en-US/Resources.resw | 637 +++++----
Strings/zh-CN/Resources.resw | 1449 ++++++++++++++------
ViewModels/CustomRulesViewModel.cs | 27 +-
ViewModels/ManageSubscriptionsViewModel.cs | 3 +-
ViewModels/ServerDetailViewModel.cs | 37 +-
ViewModels/ServerListViewModel.cs | 36 +-
Views/AddChainProxyDialog.xaml | 15 +-
Views/AddChainProxyDialog.xaml.cs | 9 +-
Views/AddRuleDialog.xaml | 28 +-
Views/AddRuleDialog.xaml.cs | 22 +-
Views/ControlPanelControl.xaml | 6 +-
Views/ControlPanelControl.xaml.cs | 2 +
Views/CustomRulesWindow.xaml | 20 +-
Views/CustomRulesWindow.xaml.cs | 17 +-
Views/LogWindow.xaml | 8 +-
Views/LogWindow.xaml.cs | 19 +-
Views/ManageSubscriptionsDialog.xaml | 35 +-
Views/ManageSubscriptionsDialog.xaml.cs | 16 +
Views/PersonalizeControl.xaml | 99 +-
Views/PersonalizeControl.xaml.cs | 29 +-
Views/ServerDetailControl.xaml | 44 +-
Views/ServerDetailControl.xaml.cs | 18 +
Views/ServerListControl.xaml | 56 +-
Views/ServerListControl.xaml.cs | 15 +-
Views/TunConfirmationDialog.xaml | 11 +-
Views/TunConfirmationDialog.xaml.cs | 7 +-
32 files changed, 1992 insertions(+), 903 deletions(-)
diff --git a/Helpers/L.cs b/Helpers/L.cs
index 14d7b02..44be8dc 100644
--- a/Helpers/L.cs
+++ b/Helpers/L.cs
@@ -1,4 +1,4 @@
-namespace XrayUI.Helpers;
+namespace XrayUI.Helpers;
///
/// Strongly-typed accessors for resource strings. Each property is the canonical
@@ -80,6 +80,7 @@ public static class L
public static string Main_NotConnected => Loc.GetString("Main_NotConnected");
// ── ControlPanel ───────────────────────────────────────────────────────
+ public static string ControlPanel_Personalize => Loc.GetString("ControlPanel_Personalize");
public static string ControlPanel_Start => Loc.GetString("ControlPanel_Start");
public static string ControlPanel_Stop => Loc.GetString("ControlPanel_Stop");
public static string ControlPanel_StatusApplying => Loc.GetString("ControlPanel_StatusApplying");
@@ -101,7 +102,144 @@ public static class L
// ── Startup / Update / TUN ─────────────────────────────────────────────
public static string Startup_SetFailed => Loc.GetString("Startup_SetFailed");
public static string Update_Updating => Loc.GetString("Update_Updating");
- public static string Tun_EnableMsg => Loc.GetString("Tun_EnableMsg");
+ public static string Tun_EnableMsg => Loc.GetString("Tun_EnableMsg.Text");
+
+ // ── ChainProxy ─────────────────────────────────────────────────────────
+ public static string ChainProxy_NameRequired => Loc.GetString("ChainProxy_NameRequired");
+ public static string ChainProxy_EntryRequired => Loc.GetString("ChainProxy_EntryRequired");
+ public static string ChainProxy_ExitRequired => Loc.GetString("ChainProxy_ExitRequired");
+ public static string ChainProxy_EntryExitSame => Loc.GetString("ChainProxy_EntryExitSame");
+
+ // ── TUN confirmation ───────────────────────────────────────────────────
+ public static string Tun_MoreOptions => Loc.GetString("Tun_MoreOptionsBtn.Content");
+ public static string Tun_InterfaceTooltip => Loc.GetString("Tun_InterfaceTooltip");
+ public static string Tun_AutoInterfaceLabel => Loc.GetString("Tun_AutoInterfaceLabel");
+
+ // ── Subscription dialog ────────────────────────────────────────────────
+ public static string Subscription_DialogTitle_Add => Loc.GetString("Subscription_DialogTitle_Add");
+ public static string Subscription_DialogTitle_Manage => Loc.GetString("Subscription_DialogTitle_Manage");
+ public static string Subscription_AddTooltip => Loc.GetString("Subscription_AddTooltip");
+ public static string Subscription_ManageTooltip => Loc.GetString("Subscription_ManageTooltip");
+ public static string Subscription_Refresh => Loc.GetString("Subscription_Refresh");
+ public static string Subscription_DeleteTooltip => Loc.GetString("Subscription_DeleteTooltip");
+ public static string Subscription_NeverUpdated => Loc.GetString("Subscription_NeverUpdated");
+ public static string Subscription_JustNow => Loc.GetString("Subscription_JustNow");
+
+ // ── Personalize ────────────────────────────────────────────────────────
+ public static string Personalize_ExportSuccess => Loc.GetString("Personalize_ExportSuccess");
+ public static string Personalize_ImportFailed => Loc.GetString("Personalize_ImportFailed");
+ public static string Personalize_ImportSuccess => Loc.GetString("Personalize_ImportSuccess");
+ public static string Personalize_ImportAdvancedSuffix => Loc.GetString("Personalize_ImportAdvancedSuffix");
+ public static string Personalize_PresetMissingTitle => Loc.GetString("Personalize_PresetMissingTitle");
+ public static string Personalize_PresetMissingMsg => Loc.GetString("Personalize_PresetMissingMsg");
+ public static string Personalize_ExportTooltip => Loc.GetString("Personalize_ExportTooltip");
+ public static string Personalize_ImportTooltip => Loc.GetString("Personalize_ImportTooltip");
+ public static string Error_ExportFailed => Loc.GetString("Error_ExportFailed");
+
+ // ── CustomRules / AddRule ──────────────────────────────────────────────
+ public static string CustomRules_Title => Loc.GetString("CustomRules_Title");
+ public static string CustomRules_UpdateGeoTooltip => Loc.GetString("CustomRules_UpdateGeoTooltip");
+ public static string CustomRules_AdvancedEditorTooltip => Loc.GetString("CustomRules_AdvancedEditorTooltip");
+ public static string CustomRules_EditRowTooltip => Loc.GetString("CustomRules_EditRowTooltip");
+ public static string CustomRules_DeleteRowTooltip => Loc.GetString("CustomRules_DeleteRowTooltip");
+ public static string CustomRules_PrepFailedTitle => Loc.GetString("CustomRules_PrepFailedTitle");
+ public static string CustomRules_OpenEditorFailedTitle => Loc.GetString("CustomRules_OpenEditorFailedTitle");
+
+ public static string AddRule_Title => Loc.GetString("AddRule_Title");
+ public static string AddRule_EditTitle => Loc.GetString("AddRule_EditTitle");
+ public static string AddRule_ErrorEmpty => Loc.GetString("AddRule_ErrorEmpty");
+ public static string AddRule_BrowseExe => Loc.GetString("AddRule_BrowseExe");
+ public static string AddRule_BrowseFolder => Loc.GetString("AddRule_BrowseFolder");
+ public static string AddRule_PlaceholderDomain => Loc.GetString("AddRule_PlaceholderDomain");
+ public static string AddRule_PlaceholderIp => Loc.GetString("AddRule_PlaceholderIp");
+ public static string AddRule_PlaceholderProcess => Loc.GetString("AddRule_PlaceholderProcess");
+ public static string AddRule_HintDomain => Loc.GetString("AddRule_HintDomain");
+ public static string AddRule_HintIp => Loc.GetString("AddRule_HintIp");
+ public static string AddRule_HintProcess => Loc.GetString("AddRule_HintProcess");
+
+ public static string GeoUpdate_Updating => Loc.GetString("GeoUpdate_Updating");
+ public static string GeoUpdate_AlreadyLatest => Loc.GetString("GeoUpdate_AlreadyLatest");
+ public static string GeoUpdate_AlreadyLatestMsg => Loc.GetString("GeoUpdate_AlreadyLatestMsg");
+ public static string GeoUpdate_TunRestart => Loc.GetString("GeoUpdate_TunRestart");
+ public static string GeoUpdate_ReloadedOk => Loc.GetString("GeoUpdate_ReloadedOk");
+ public static string GeoUpdate_RestartRequired => Loc.GetString("GeoUpdate_RestartRequired");
+ public static string GeoUpdate_NextStart => Loc.GetString("GeoUpdate_NextStart");
+ public static string GeoUpdate_Success => Loc.GetString("GeoUpdate_Success");
+
+ // ── LogWindow ──────────────────────────────────────────────────────────
+ public static string Log_Title => Loc.GetString("Log_Title");
+ public static string Log_Running => Loc.GetString("Log_Running");
+ public static string Log_NotRunning => Loc.GetString("Log_NotRunning");
+ public static string Log_PrivacyTitle => Loc.GetString("Log_PrivacyTitle");
+ public static string Log_PrivacySaved => Loc.GetString("Log_PrivacySaved");
+ public static string Log_IpMask => Loc.GetString("Log_IpMask");
+ public static string Log_MaskOff => Loc.GetString("Log_MaskOff");
+ public static string Log_AutoScroll => Loc.GetString("Log_AutoScroll");
+ public static string Log_CopyAll => Loc.GetString("Log_CopyAll");
+ public static string Log_Clear => Loc.GetString("Log_Clear");
+ public static string Log_PrivacyTooltip => Loc.GetString("Log_PrivacyTooltip");
+
+ // ── MainWindow / Tray ──────────────────────────────────────────────────
+ public static string MainWindow_Title => Loc.GetString("MainWindow_Title");
+ public static string MainWindow_ToggleMini => Loc.GetString("MainWindow_ToggleMini");
+ public static string MainWindow_ExpandFull => Loc.GetString("MainWindow_ExpandFull");
+ public static string MainWindow_Close => Loc.GetString("MainWindow_Close");
+ public static string Tray_Open => Loc.GetString("Tray_Open");
+ public static string Tray_Exit => Loc.GetString("Tray_Exit");
+
+ // ── ServerList ─────────────────────────────────────────────────────────
+ public static string ServerList_AllServers => Loc.GetString("ServerList_AllServers");
+ public static string ServerList_Ungrouped => Loc.GetString("ServerList_Ungrouped");
+ public static string ServerList_Favorites => Loc.GetString("ServerList_Favorites");
+ public static string ServerList_UnnamedSub => Loc.GetString("ServerList_UnnamedSub");
+ public static string ServerList_OrphanSub => Loc.GetString("ServerList_OrphanSub");
+ public static string ServerList_Edit => Loc.GetString("ServerList_Edit");
+ public static string ServerList_Delete => Loc.GetString("ServerList_Delete");
+ public static string ServerList_Share => Loc.GetString("ServerList_Share");
+ public static string ServerList_AddFavorite => Loc.GetString("ServerList_AddFavorite");
+ public static string ServerList_RemoveFavorite => Loc.GetString("ServerList_RemoveFavorite");
+ public static string ServerList_FilterTooltip => Loc.GetString("ServerList_FilterTooltip");
+ public static string ServerList_SortTooltip => Loc.GetString("ServerList_SortTooltip");
+ public static string ServerList_SortActiveHint => Loc.GetString("ServerList_SortActiveHint");
+
+ // ── ServerDetail ──────────────────────────────────────────────────────
+ public static string ServerDetail_NoServer => Loc.GetString("ServerDetail_NoServer");
+ public static string ServerDetail_Address => Loc.GetString("ServerDetail_Address");
+ public static string ServerDetail_Port => Loc.GetString("ServerDetail_Port");
+ public static string ServerDetail_Encryption => Loc.GetString("ServerDetail_Encryption");
+ public static string ServerDetail_Security => Loc.GetString("ServerDetail_Security");
+ public static string ServerDetail_Entry => Loc.GetString("ServerDetail_Entry");
+ public static string ServerDetail_Exit => Loc.GetString("ServerDetail_Exit");
+ public static string ServerDetail_EntryMissing => Loc.GetString("ServerDetail_EntryMissing");
+ public static string ServerDetail_ExitMissing => Loc.GetString("ServerDetail_ExitMissing");
+ public static string ServerDetail_AuthLabel => Loc.GetString("ServerDetail_AuthLabel");
+ public static string ServerDetail_ChainLabel => Loc.GetString("ServerDetail_ChainLabel");
+ public static string ServerDetail_NoAuth => Loc.GetString("ServerDetail_NoAuth");
+ public static string ServerDetail_UserPass => Loc.GetString("ServerDetail_UserPass");
+ public static string ServerDetail_NotTested => Loc.GetString("ServerDetail_NotTested");
+ public static string ServerDetail_Testing => Loc.GetString("ServerDetail_Testing");
+ public static string ServerDetail_Timeout => Loc.GetString("ServerDetail_Timeout");
+ public static string ServerDetail_Failed => Loc.GetString("ServerDetail_Failed");
+ public static string ServerDetail_RetestLatency => Loc.GetString("ServerDetail_RetestLatency");
+ public static string ServerDetail_CopyShareLink => Loc.GetString("ServerDetail_CopyShareLink");
+
+ // ── Import link dialog ────────────────────────────────────────────────
+ public static string Import_ParseFailed => Loc.GetString("Import_ParseFailed");
+ public static string Import_ParseFailedMsg => Loc.GetString("Import_ParseFailedMsg");
+
+ // ── Subscription ──────────────────────────────────────────────────────
+ public static string Subscription_FetchFailed => Loc.GetString("Subscription_FetchFailed");
+ public static string Subscription_NoParsed => Loc.GetString("Subscription_NoParsed");
+ public static string Subscription_StopFirst_Refresh => Loc.GetString("Subscription_StopFirst_Refresh");
+ public static string Subscription_StopFirst_Delete => Loc.GetString("Subscription_StopFirst_Delete");
+ public static string Subscription_UnknownError => Loc.GetString("Subscription_UnknownError");
+
+ // ── Share dialog ──────────────────────────────────────────────────────
+ public static string Share_NotSupported => Loc.GetString("Share_NotSupported");
+ public static string Share_NotSupportedMsg => Loc.GetString("Share_NotSupportedMsg");
+
+ // ── Confirm delete ────────────────────────────────────────────────────
+ public static string Confirm_DeleteTitle => Loc.GetString("Confirm_DeleteTitle");
// ── DNS settings dialog ────────────────────────────────────────────────
public static string Dns_DialogTitle => Loc.GetString("Dns_DialogTitle");
diff --git a/Helpers/LanguageHelper.cs b/Helpers/LanguageHelper.cs
index 537ce6d..400b3e9 100644
--- a/Helpers/LanguageHelper.cs
+++ b/Helpers/LanguageHelper.cs
@@ -8,15 +8,30 @@ 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.
+ ///
+ /// Real language rows pass a fixed endonym
+ /// (the language's name in its own script — "简体中文", "English") so that
+ /// users always see their own language spelled the way they recognize it,
+ /// regardless of the currently-applied UI locale. The "follow system" row is
+ /// the exception: it isn't a language name, it's an action description, so it
+ /// passes a resourceKey and the display string is resolved against the
+ /// current UI locale instead.
///
public sealed partial class LanguageInfo
{
+ private readonly string _displayName;
+ private readonly string? _resourceKey;
+
public string? Tag { get; }
- public string DisplayName { get; }
- public LanguageInfo(string? tag, string displayName)
+
+ public string DisplayName =>
+ _resourceKey is null ? _displayName : Loc.GetString(_resourceKey);
+
+ public LanguageInfo(string? tag, string displayName, string? resourceKey = null)
{
Tag = tag;
- DisplayName = displayName;
+ _displayName = displayName;
+ _resourceKey = resourceKey;
}
}
@@ -33,7 +48,9 @@ public static class LanguageHelper
{
public static readonly LanguageInfo[] SupportedLanguages =
[
- new(null, "跟随系统"), // index 0 — leaves PrimaryLanguageOverride alone
+ // Index 0 — "follow system" is an action label, not a language name, so
+ // it must follow the current UI locale (not stay in Chinese forever).
+ new(null, "跟随系统", resourceKey: "Language_FollowSystem"),
new("zh-CN", "简体中文"),
new("en-US", "English"),
];
diff --git a/MainWindow.xaml b/MainWindow.xaml
index f903f83..603afc7 100644
--- a/MainWindow.xaml
+++ b/MainWindow.xaml
@@ -25,7 +25,8 @@
-
@@ -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 @@
-
-
diff --git a/Views/CustomRulesWindow.xaml.cs b/Views/CustomRulesWindow.xaml.cs
index 9777d2c..a399877 100644
--- a/Views/CustomRulesWindow.xaml.cs
+++ b/Views/CustomRulesWindow.xaml.cs
@@ -34,9 +34,12 @@ public CustomRulesWindow(Window owner, CustomRulesViewModel viewModel)
AppWindow.Resize(new SizeInt32(
(int)Math.Round(620 * scale),
(int)Math.Round(460 * scale)));
- AppWindow.Title = "自定义路由规则";
+ AppWindow.Title = L.CustomRules_Title;
AppWindow.TitleBar.PreferredTheme = TitleBarTheme.UseDefaultAppMode;
+ ToolTipService.SetToolTip(OpenAdvancedEditorButton, L.CustomRules_AdvancedEditorTooltip);
+ ToolTipService.SetToolTip(UpdateGeoButton, L.CustomRules_UpdateGeoTooltip);
+
var presenter = OverlappedPresenter.CreateForDialog();
// 1. Set Win32 owner BEFORE IsModal — IsModal requires an owner.
@@ -120,6 +123,18 @@ private async void OnShowAddOrEditDialogRequested(object? sender, CustomRoutingR
ViewModel.ReplaceRule(existing, dialog.Result);
}
+ private void EditRuleButton_Loaded(object sender, RoutedEventArgs e)
+ {
+ if (sender is FrameworkElement element)
+ ToolTipService.SetToolTip(element, L.CustomRules_EditRowTooltip);
+ }
+
+ private void DeleteRuleButton_Loaded(object sender, RoutedEventArgs e)
+ {
+ if (sender is FrameworkElement element)
+ ToolTipService.SetToolTip(element, L.CustomRules_DeleteRowTooltip);
+ }
+
private void EditRuleButton_Click(object sender, RoutedEventArgs e)
{
if (sender is FrameworkElement { DataContext: CustomRoutingRule rule })
diff --git a/Views/LogWindow.xaml b/Views/LogWindow.xaml
index 2edcffc..3bfaca2 100644
--- a/Views/LogWindow.xaml
+++ b/Views/LogWindow.xaml
@@ -61,13 +61,12 @@
Orientation="Horizontal"
Spacing="8">
@@ -396,9 +410,11 @@
-
-
@@ -436,7 +452,8 @@
-
@@ -461,9 +478,11 @@
-
-
@@ -479,9 +498,11 @@
Margin="42,10,16,10">
-
-
@@ -498,9 +519,11 @@
Margin="42,10,16,10">
-
-
@@ -512,7 +535,8 @@
-
@@ -537,9 +561,11 @@
-
-
@@ -549,15 +575,15 @@
Orientation="Horizontal"
Spacing="8"
VerticalAlignment="Center">
-
-
@@ -570,7 +596,8 @@
-
-
-
-
-
-
@@ -188,11 +193,10 @@
FontSize="12"
FontWeight="SemiBold"
VerticalAlignment="Center" />
-
-
-
-
@@ -397,13 +400,13 @@
VerticalAlignment="Center"
Margin="0,0,8,0" />
-
@@ -411,14 +414,13 @@
-
diff --git a/Views/ServerDetailControl.xaml.cs b/Views/ServerDetailControl.xaml.cs
index ffa1e47..f383879 100644
--- a/Views/ServerDetailControl.xaml.cs
+++ b/Views/ServerDetailControl.xaml.cs
@@ -1,5 +1,7 @@
using System;
+using Microsoft.UI.Xaml.Automation;
using Windows.System;
+using XrayUI.Helpers;
namespace XrayUI.Views
{
@@ -15,6 +17,22 @@ public sealed partial class ServerDetailControl
public ServerDetailControl()
{
this.InitializeComponent();
+ ApplyLocalizedAttachedProperties();
+ }
+
+ private void ApplyLocalizedAttachedProperties()
+ {
+ void SetTooltipAndName(FrameworkElement element, string text)
+ {
+ ToolTipService.SetToolTip(element, text);
+ AutomationProperties.SetName(element, text);
+ }
+
+ SetTooltipAndName(OpenAiLinkButton, Loc.Format("ServerDetail_OpenInBrowser", "OpenAI"));
+ SetTooltipAndName(ClaudeLinkButton, Loc.Format("ServerDetail_OpenInBrowser", "Claude"));
+ SetTooltipAndName(GeminiLinkButton, Loc.Format("ServerDetail_OpenInBrowser", "Gemini"));
+ ToolTipService.SetToolTip(RetestLatencyButton, L.ServerDetail_RetestLatency);
+ SetTooltipAndName(CopyShareLinkButton, L.ServerDetail_CopyShareLink);
}
private void ShadowRect_Loaded(object sender, RoutedEventArgs e)
diff --git a/Views/ServerListControl.xaml b/Views/ServerListControl.xaml
index fd23055..caa3dd6 100644
--- a/Views/ServerListControl.xaml
+++ b/Views/ServerListControl.xaml
@@ -20,7 +20,8 @@
-
-
+
-
-
-
-
@@ -74,7 +79,7 @@
-
+
-
+
@@ -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}");
}
}
}