From c538f41f6d3bf90797732bdc7f3d0989e9e9b711 Mon Sep 17 00:00:00 2001 From: Junyi Ou Date: Tue, 23 Jun 2026 15:28:08 -0400 Subject: [PATCH 1/2] chore(agent-installer): track custom installer dialog localization gap --- package/AgentWindowsManaged/Program.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/package/AgentWindowsManaged/Program.cs b/package/AgentWindowsManaged/Program.cs index febd3f055..ba68e4dac 100644 --- a/package/AgentWindowsManaged/Program.cs +++ b/package/AgentWindowsManaged/Program.cs @@ -439,6 +439,17 @@ private static void Project_UIInitialized(SetupEventArgs e) strings.Add(s.Attributes["Id"].Value, s.InnerText); } + // TODO(agent-tunnel): these strings are loaded into a LOCAL dict that only feeds the + // pre-flight MessageBoxes below (x86 / .NET 4.8 / newer-installed). The custom dialogs + // (AgentTunnelDialog, AgentDialog title, etc.) resolve their "[Key]" labels via + // MsiRuntime.Localize, which is NOT populated from these custom strings — light.exe only + // emits strings referenced via !(loc.X) into the MSI, and the custom "[Key]" labels are + // never !(loc.X)-referenced, so they fall back to the raw key name in the UI + // (e.g. "AgentTunnelDlgTitle" shows literally). Wire this `strings` dict into the + // ManagedUI runtime localization (or have the custom dialogs use a shared I18n backed by + // it) so the labels render. Standard dialogs (Welcome/InstallDir) work only because + // WixSharp's built-in UI references those standard IDs via !(loc.X). + string I18n(string key) { if (!strings.TryGetValue(key, out string result)) From eae349f2a94a660bce7fc265ad118d4a04ea570f Mon Sep 17 00:00:00 2001 From: Junyi Ou Date: Wed, 24 Jun 2026 12:23:47 -0400 Subject: [PATCH 2/2] fix(agent-installer): localize custom installer dialog labels Custom WiX dialogs (the Agent Tunnel dialog and the base dialog titles) displayed raw localization keys such as "AgentTunnelDlgTitle" instead of translated text. light.exe only emits !(loc.X)-referenced strings into the MSI localization tables, and the custom "[Key]" labels are never referenced that way, so they fell back to the raw key name in both en-US and fr-FR. Load the embedded .wxl into the managed UI runtime localization table (MsiRuntime.UIText via InitFromWxl) when the UI initializes, so the custom dialogs resolve their [Key] labels through MsiRuntime.Localize. The pre-flight message boxes read from the same table. --- package/AgentWindowsManaged/Program.cs | 44 +++++++++----------------- 1 file changed, 15 insertions(+), 29 deletions(-) diff --git a/package/AgentWindowsManaged/Program.cs b/package/AgentWindowsManaged/Program.cs index ba68e4dac..b02f54176 100644 --- a/package/AgentWindowsManaged/Program.cs +++ b/package/AgentWindowsManaged/Program.cs @@ -13,9 +13,7 @@ using System.IO.Compression; using System.Linq; using System.Security.Cryptography; -using System.Text.RegularExpressions; using System.Windows.Forms; -using System.Xml; using WixSharp; using WixSharp.CommonTasks; @@ -425,44 +423,32 @@ private static void Project_UnhandledException(ExceptionEventArgs e) private static void Project_UIInitialized(SetupEventArgs e) { string lcid = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName == "fr" ? frFR.Key : enUS.Key; + string resourceName = $"DevolutionsAgent.Resources.{Languages[lcid]}"; - using Stream stream = Assembly.GetExecutingAssembly() - .GetManifestResourceStream($"DevolutionsAgent.Resources.{Languages[lcid]}"); + using Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resourceName); - XmlDocument xml = new(); - xml.Load(stream); + if (stream is null) + { + throw new FileNotFoundException($"Missing localization resource: {resourceName}"); + } - Dictionary strings = new(); + using MemoryStream memory = new(); + stream.CopyTo(memory); - foreach (XmlNode s in xml.GetElementsByTagName("String")) + if (e.ManagedUI.Shell.RuntimeContext is not InstallerRuntime runtime) { - strings.Add(s.Attributes["Id"].Value, s.InnerText); + throw new InvalidOperationException("Managed UI runtime is not available"); } - // TODO(agent-tunnel): these strings are loaded into a LOCAL dict that only feeds the - // pre-flight MessageBoxes below (x86 / .NET 4.8 / newer-installed). The custom dialogs - // (AgentTunnelDialog, AgentDialog title, etc.) resolve their "[Key]" labels via - // MsiRuntime.Localize, which is NOT populated from these custom strings — light.exe only - // emits strings referenced via !(loc.X) into the MSI, and the custom "[Key]" labels are - // never !(loc.X)-referenced, so they fall back to the raw key name in the UI - // (e.g. "AgentTunnelDlgTitle" shows literally). Wire this `strings` dict into the - // ManagedUI runtime localization (or have the custom dialogs use a shared I18n backed by - // it) so the labels render. Standard dialogs (Welcome/InstallDir) work only because - // WixSharp's built-in UI references those standard IDs via !(loc.X). + runtime.UIText.InitFromWxl(memory.ToArray(), true); string I18n(string key) { - if (!strings.TryGetValue(key, out string result)) - { - return key; - } - - return Regex.Replace(result, @"\[(.*?)]", (match) => + string localized = $"[{key}]".LocalizeWith(runtime.Localize); + return localized.LocalizeWith(name => { - string property = match.Groups[1].Value; - string value = e.Session[property]; - - return string.IsNullOrEmpty(value) ? property : value; + string value = e.Session[name]; + return string.IsNullOrEmpty(value) ? null : value; }); }