From 3aaeb60900cb054ffa4234b8119932960fb6dbf1 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 2583b508ca7a4616a3d564a4d091ff04901ff51c 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 | 46 +++++--------------------- 1 file changed, 8 insertions(+), 38 deletions(-) diff --git a/package/AgentWindowsManaged/Program.cs b/package/AgentWindowsManaged/Program.cs index ba68e4dac..8b2c5e26d 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; @@ -427,44 +425,16 @@ private static void Project_UIInitialized(SetupEventArgs e) string lcid = CultureInfo.CurrentUICulture.TwoLetterISOLanguageName == "fr" ? frFR.Key : enUS.Key; using Stream stream = Assembly.GetExecutingAssembly() - .GetManifestResourceStream($"DevolutionsAgent.Resources.{Languages[lcid]}"); + .GetManifestResourceStream($"DevolutionsAgent.Resources.{Languages[lcid]}") + ?? throw new FileNotFoundException($"Missing localization resource: {Languages[lcid]}"); + using MemoryStream memory = new(); + stream.CopyTo(memory); - XmlDocument xml = new(); - xml.Load(stream); + InstallerRuntime runtime = e.ManagedUI.Shell.RuntimeContext as InstallerRuntime + ?? throw new InvalidOperationException("Managed UI runtime is not available"); + runtime.UIText.InitFromWxl(memory.ToArray(), true); - Dictionary strings = new(); - - foreach (XmlNode s in xml.GetElementsByTagName("String")) - { - 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)) - { - return key; - } - - return Regex.Replace(result, @"\[(.*?)]", (match) => - { - string property = match.Groups[1].Value; - string value = e.Session[property]; - - return string.IsNullOrEmpty(value) ? property : value; - }); - } + string I18n(string key) => $"[{key}]".LocalizeWith(runtime.Localize); if (!Environment.Is64BitOperatingSystem) {