From 590b151706f754db0872fbdefc0a0219e5942998 Mon Sep 17 00:00:00 2001 From: Copilot Date: Tue, 7 Jul 2026 13:12:14 -0700 Subject: [PATCH] Fix runtime localization of XAML property keys Resolve XAML property resource keys passed through LocalizationHelper by falling back from dotted keys to MRT path segments. This prevents runtime-updated controls from displaying raw keys such as ConnectionPage_Connect2.Content. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../Helpers/LocalizationHelper.cs | 61 ++++++++++++++++--- .../AssistantBridgeServiceTests.cs | 2 +- .../LocalizationHelperSourceTests.cs | 20 ++++++ 3 files changed, 74 insertions(+), 9 deletions(-) create mode 100644 tests/OpenClaw.Tray.Tests/LocalizationHelperSourceTests.cs diff --git a/src/OpenClaw.Tray.WinUI/Helpers/LocalizationHelper.cs b/src/OpenClaw.Tray.WinUI/Helpers/LocalizationHelper.cs index 189b40ca3..53bf2887d 100644 --- a/src/OpenClaw.Tray.WinUI/Helpers/LocalizationHelper.cs +++ b/src/OpenClaw.Tray.WinUI/Helpers/LocalizationHelper.cs @@ -41,28 +41,73 @@ private static ResourceContext GetContext() public static string GetString(string resourceKey) { - try + var found = TryGetValueAsString(resourceKey, out var value, out var lookupFailure); + if (found && !string.IsNullOrEmpty(value)) { - var candidate = Manager.MainResourceMap.GetValue($"Resources/{resourceKey}", GetContext()); - var value = candidate?.ValueAsString; - return string.IsNullOrEmpty(value) ? resourceKey : value; + return value; } - catch (Exception ex) + + if (TryGetXamlPropertyResourcePath(resourceKey, out var propertyResourcePath)) { - var logKey = $"{_languageOverride ?? ""}:{resourceKey}:{ex.GetType().FullName}"; + var propertyFound = TryGetValueAsString(propertyResourcePath, out value, out var propertyLookupFailure); + if (propertyFound && !string.IsNullOrEmpty(value)) + { + return value; + } + + if (!found) + { + lookupFailure ??= propertyLookupFailure; + } + } + + if (lookupFailure is not null) + { + var logKey = $"{_languageOverride ?? ""}:{resourceKey}:{lookupFailure.GetType().FullName}"; if (s_loggedLookupFailures.ContainsKey(logKey)) return resourceKey; if (s_loggedLookupFailures.Count < MaxLoggedLookupFailures && s_loggedLookupFailures.TryAdd(logKey, 0)) { - Logger.Warn($"LocalizationHelper: Resource lookup failed for '{resourceKey}' (language='{_languageOverride ?? ""}'): {ex.Message}"); + Logger.Warn($"LocalizationHelper: Resource lookup failed for '{resourceKey}' (language='{_languageOverride ?? ""}'): {lookupFailure.Message}"); } else if (System.Threading.Interlocked.Exchange(ref s_lookupFailureLimitLogged, 1) == 0) { Logger.Warn("LocalizationHelper: Resource lookup failure log limit reached; suppressing additional unique resource lookup failures"); } - return resourceKey; } + + return resourceKey; + } + + private static bool TryGetValueAsString(string resourceKey, out string? value, out Exception? lookupFailure) + { + try + { + var candidate = Manager.MainResourceMap.GetValue($"Resources/{resourceKey}", GetContext()); + value = candidate?.ValueAsString; + lookupFailure = null; + return true; + } + catch (Exception ex) + { + value = null; + lookupFailure = ex; + return false; + } + } + + private static bool TryGetXamlPropertyResourcePath(string resourceKey, out string resourcePath) + { + var propertySeparator = resourceKey.LastIndexOf('.'); + if (propertySeparator > 0 && propertySeparator < resourceKey.Length - 1) + { + resourcePath = $"{resourceKey[..propertySeparator]}/{resourceKey[(propertySeparator + 1)..]}"; + return true; + } + + resourcePath = string.Empty; + return false; } /// diff --git a/tests/OpenClaw.Tray.Tests/AssistantBridgeServiceTests.cs b/tests/OpenClaw.Tray.Tests/AssistantBridgeServiceTests.cs index 7291729f4..498cfe49f 100644 --- a/tests/OpenClaw.Tray.Tests/AssistantBridgeServiceTests.cs +++ b/tests/OpenClaw.Tray.Tests/AssistantBridgeServiceTests.cs @@ -202,7 +202,7 @@ public async Task StartListenServiceAsync_KillsTimedOutBackendCommand() NullLogger.Instance, "owner", () => launcher, - TimeSpan.FromSeconds(2)); + TimeSpan.FromSeconds(5)); var result = await service.StartListenServiceAsync(); diff --git a/tests/OpenClaw.Tray.Tests/LocalizationHelperSourceTests.cs b/tests/OpenClaw.Tray.Tests/LocalizationHelperSourceTests.cs new file mode 100644 index 000000000..c0090c83c --- /dev/null +++ b/tests/OpenClaw.Tray.Tests/LocalizationHelperSourceTests.cs @@ -0,0 +1,20 @@ +using System.IO; + +namespace OpenClaw.Tray.Tests; + +public sealed class LocalizationHelperSourceTests +{ + [Fact] + public void GetString_ResolvesXamlPropertyResourceKeys() + { + var source = ReadSource("src", "OpenClaw.Tray.WinUI", "Helpers", "LocalizationHelper.cs"); + + Assert.Contains("TryGetXamlPropertyResourcePath(resourceKey, out var propertyResourcePath)", source); + Assert.Contains("TryGetValueAsString(propertyResourcePath", source); + Assert.Contains("LastIndexOf('.')", source); + Assert.Contains("\"{resourceKey[..propertySeparator]}/{resourceKey[(propertySeparator + 1)..]}\"", source); + } + + private static string ReadSource(params string[] parts) + => File.ReadAllText(Path.Combine(new[] { TestRepositoryPaths.GetRepositoryRoot() }.Concat(parts).ToArray())); +}