diff --git a/App.xaml.cs b/App.xaml.cs index d43e459..c355a95 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,30 @@ public App() AppDomain.CurrentDomain.ProcessExit += (_, _) => CleanupOnExit(); } + private static void ApplyPersistedLanguageOverride() + { + string? language = null; + try + { + if (File.Exists(AppPaths.SettingsJsonPath)) + { + 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) { var cmdArgs = Environment.GetCommandLineArgs(); @@ -89,6 +121,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..44be8dc --- /dev/null +++ b/Helpers/L.cs @@ -0,0 +1,262 @@ +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 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. +/// +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_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"); + 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.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"); + 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/Helpers/LanguageHelper.cs b/Helpers/LanguageHelper.cs new file mode 100644 index 0000000..59efe51 --- /dev/null +++ b/Helpers/LanguageHelper.cs @@ -0,0 +1,117 @@ +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. + /// + /// 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 => + _resourceKey is null ? _displayName : Loc.GetString(_resourceKey); + + public LanguageInfo(string? tag, string displayName, string? resourceKey = null) + { + Tag = tag; + _displayName = displayName; + _resourceKey = resourceKey; + } + } + + /// + /// 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 = + [ + // 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"), + ]; + + /// + /// 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; + + /// + /// 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); + + try + { + ApplicationLanguages.PrimaryLanguageOverride = language ?? string.Empty; + } + catch (Exception ex) + { + var label = string.IsNullOrEmpty(language) ? "follow system" : language; + Debug.WriteLine($"[Language] Failed to apply '{label}': {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/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/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/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/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/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/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 new file mode 100644 index 0000000..0d2e4c3 --- /dev/null +++ b/Strings/en-US/Resources.resw @@ -0,0 +1,1081 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + Proxy Panel + + + Toggle mini window + + + Back to full window + + + Close + + + Open + + + Exit + + + Edit + + + Delete + + + Share + + + Only available when filter is "All Servers" + + + All Servers + + + Ungrouped + + + (Unnamed Subscription) + + + (Deleted Subscription) + + + Start + + + Stop + + + Idle + + + Applying... + + + Personalize + + + Global + + + Smart + + + New version {0} + + + Address + + + Port + + + Encryption + + + Security + + + Open {0} in browser + + + Retest latency + + + Copy share link + + + No server selected + + + Not tested + + + Testing... + + + Timeout + + + Failed + + + Personalization + + + Theme + + + Light + + + Dark + + + System + + + Background Effect + + + Acrylic + + + Protocol Colors + + + Reset + + + Other Protocols + + + Unmatched protocols will use this color + + + Data Management + + + Done + + + Export Successful + + + Follow System + Display name of the "follow system" language row — referenced by LanguageInfo.DisplayName when Tag is null. + + + Add Subscription + + + Manage Subscriptions + + + Refresh Subscription + + + Delete Subscription + + + 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 + + + Update GeoFile routing data + + + Add Rule + + + 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 + + + No valid node links found. Please verify your input and try again. + + + Add Server Manually + + + Edit Server + + + Name + + + Address / Domain + + + Port + + + Protocol + + + Encryption (SS) + + + Password + + + 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 + + + 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... + + + Add 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 directly without proxy + + + Proxy DNS + + + For foreign domains, resolved via proxy outbound to prevent DNS hijacking + + + 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 + + + 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. + + + 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 + + + Last updated: never + + + just now + + + {0} min ago + + + {0} hr ago + + + {0} days ago + + + Last updated: {0} + + + Edit Port + + + Logs + + + Proxy Setting + + + System Proxy + + + Bypass System Proxy + + + Startup + + + Routing + + + Global + + + Smart + + + Custom Rules... + + + TUN Mode + + + Routing: + + + 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 new file mode 100644 index 0000000..584635d --- /dev/null +++ b/Strings/zh-CN/Resources.resw @@ -0,0 +1,1081 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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/ControlPanelViewModel.cs b/ViewModels/ControlPanelViewModel.cs index d571661..bc8b252 100644 --- a/ViewModels/ControlPanelViewModel.cs +++ b/ViewModels/ControlPanelViewModel.cs @@ -20,12 +20,12 @@ 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; private int _localPort = 16890; - private string _routingMode = "智能分流"; + private string _routingMode = "smart"; private bool _isSystemProxyEnabled = true; private bool _isStartupEnabled; private bool _isAutoConnect; @@ -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,13 +224,13 @@ 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; } 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; @@ -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); } /// @@ -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; @@ -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); } @@ -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" ? L.ControlPanel_RoutingGlobal : L.ControlPanel_RoutingSmart; + [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. @@ -818,7 +830,7 @@ private async Task OpenStartupSettings() } catch (Exception ex) { - await _dialogs.ShowErrorAsync("开机启动设置失败", ex.Message); + await _dialogs.ShowErrorAsync(L.Startup_SetFailed, ex.Message); return; } @@ -881,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). @@ -904,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); @@ -917,7 +929,7 @@ await _dialogs.ShowProgressBarDialogAsync("正在更新 XrayUI", } catch (Exception ex) { - await _dialogs.ShowErrorAsync("更新失败", ex.Message); + await _dialogs.ShowErrorAsync(L.Error_UpdateFailed, ex.Message); return; } @@ -932,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/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/MainViewModel.cs b/ViewModels/MainViewModel.cs index 6129ac4..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.RoutingMode; + 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( @@ -115,10 +116,11 @@ 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); + Personalize.LoadLanguage(s); ServerDetail.ShowLatencyInDetails = s.ShowLatencyInDetails; ServerDetail.ShowAiUnlockInDetails = s.ShowAiUnlockInDetails; 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/PersonalizeViewModel.cs b/ViewModels/PersonalizeViewModel.cs index d7364d4..a773b29 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; @@ -163,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; @@ -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/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 e754089..3383e29 100644 --- a/Views/ControlPanelControl.xaml +++ b/Views/ControlPanelControl.xaml @@ -23,12 +23,12 @@ VerticalAlignment="Center" Spacing="2"> - @@ -51,42 +51,53 @@ Command="{x:Bind ViewModel.UpdateAppCommand}" Visibility="{x:Bind ViewModel.UpdateBadgeVisibility, Mode=OneWay}" /> - - - - + - + + CommandParameter="manual" /> - - - + - + + CommandParameter="smart" /> - - @@ -157,7 +168,8 @@ Spacing="8" VerticalAlignment="Center" HorizontalAlignment="Center"> - - - diff --git a/Views/ControlPanelControl.xaml.cs b/Views/ControlPanelControl.xaml.cs index 889ffc4..b373537 100644 --- a/Views/ControlPanelControl.xaml.cs +++ b/Views/ControlPanelControl.xaml.cs @@ -1,5 +1,6 @@ using System; using Windows.System; +using XrayUI.Helpers; namespace XrayUI.Views { @@ -13,6 +14,7 @@ public sealed partial class ControlPanelControl public ControlPanelControl() { this.InitializeComponent(); + ToolTipService.SetToolTip(PersonalizeButton, L.ControlPanel_Personalize); } // Called by MainWindow after ViewModel is assigned (via x:Bind the property is set before Loaded) diff --git a/Views/CustomRulesWindow.xaml b/Views/CustomRulesWindow.xaml index b535724..00bb65b 100644 --- a/Views/CustomRulesWindow.xaml +++ b/Views/CustomRulesWindow.xaml @@ -24,7 +24,8 @@ - @@ -36,7 +37,6 @@ Background="Transparent" BorderThickness="0" VerticalAlignment="Center" - ToolTipService.ToolTip="高级编辑 (用外部编辑器打开 settings.json)" Command="{x:Bind ViewModel.OpenAdvancedEditorCommand}"> @@ -45,7 +45,6 @@ Background="Transparent" BorderThickness="0" VerticalAlignment="Center" - ToolTipService.ToolTip="更新GeoFiles路由数据" Click="UpdateGeoButton_Click"> @@ -53,7 +52,7 @@ Command="{x:Bind ViewModel.AddRuleCommand}"> - + @@ -73,7 +72,8 @@ FontSize="14" Foreground="{ThemeResource SystemFillColorCautionBrush}" VerticalAlignment="Center" /> - @@ -154,14 +154,14 @@ @@ -179,11 +179,13 @@ - @@ -99,6 +104,7 @@ - + VerticalAlignment="Center"> @@ -146,7 +152,8 @@ - @@ -163,24 +170,27 @@ - + BorderBrush="Transparent"> - - - + @@ -227,7 +237,8 @@ Visibility="{x:Bind IsActive, Mode=OneWay, Converter={StaticResource BooleanToVisibilityConverter}}" Grid.Column="1" Margin="8,0,0,0"> - @@ -248,7 +259,8 @@ FontWeight="SemiBold" Foreground="{x:Bind Protocol, Mode=OneWay, Converter={StaticResource ProtocolToBrushConverter}}" /> - ViewModel.EditServerCommand.Execute(null); var isFavorite = ViewModel.SelectedServer?.IsFavorite == true; var favoriteItem = CreateMenuItem( - isFavorite ? "取消收藏" : "加入收藏", + isFavorite ? L.ServerList_RemoveFavorite : L.ServerList_AddFavorite, isFavorite ? "\uE8D9" : "\uE734"); favoriteItem.Click += (_, _) => ViewModel.ToggleFavoriteCommand.Execute(null); - var deleteItem = CreateMenuItem("删除", "\uE74D"); + var deleteItem = CreateMenuItem(L.ServerList_Delete, ""); deleteItem.IsEnabled = ViewModel.CanRemoveSelectedServer; deleteItem.Click += (_, _) => ViewModel.RemoveServerCommand.Execute(null); - var shareItem = CreateMenuItem("分享", "\uE72D"); + var shareItem = CreateMenuItem(L.ServerList_Share, ""); shareItem.Click += (_, _) => ViewModel.ShareServerCommand.Execute(null); flyout.Items.Add(editItem); diff --git a/Views/TunConfirmationDialog.xaml b/Views/TunConfirmationDialog.xaml index 370d775..7be5ee2 100644 --- a/Views/TunConfirmationDialog.xaml +++ b/Views/TunConfirmationDialog.xaml @@ -10,9 +10,11 @@ - @@ -46,7 +49,8 @@ - @@ -54,8 +58,7 @@ + SelectedIndex="0" /> diff --git a/Views/TunConfirmationDialog.xaml.cs b/Views/TunConfirmationDialog.xaml.cs index d4e5580..72e97d1 100644 --- a/Views/TunConfirmationDialog.xaml.cs +++ b/Views/TunConfirmationDialog.xaml.cs @@ -4,17 +4,20 @@ using System.Net.NetworkInformation; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using XrayUI.Helpers; using XrayUI.Services; namespace XrayUI.Views { public sealed partial class TunConfirmationDialog : UserControl { - private const string AutoInterfaceLabel = "auto(推荐)"; + // Localized at construction so the resource loader is already initialized. + private static string AutoInterfaceLabel => L.Tun_AutoInterfaceLabel; public TunConfirmationDialog(int currentMtu, string currentInterface) { this.InitializeComponent(); + ToolTipService.SetToolTip(InterfaceComboBox, L.Tun_InterfaceTooltip); MtuNumberBox.Value = currentMtu; PopulateInterfaceComboBox(currentInterface); } @@ -31,7 +34,7 @@ private void MoreOptionsButton_Click(object sender, RoutedEventArgs e) if (AdvancedSettingsPanel.Visibility == Visibility.Visible) { AdvancedSettingsPanel.Visibility = Visibility.Collapsed; - MoreOptionsButton.Content = "更多选项"; + MoreOptionsButton.Content = L.Tun_MoreOptions; } else { diff --git a/XrayUI-dev.csproj b/XrayUI-dev.csproj index 37c77d8..cfee0cb 100644 --- a/XrayUI-dev.csproj +++ b/XrayUI-dev.csproj @@ -11,6 +11,10 @@ true false true + + en-US enable true None @@ -72,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