Skip to content

Commit afaaa04

Browse files
authored
feat: add localization infrastructure with English and Chinese resources (#46)
Add WinUI .resw resource files for proper multi-language support: - Strings/en-us/Resources.resw with all extractable UI strings - Strings/zh-cn/Resources.resw with Chinese Simplified translations - x:Uid attributes on XAML elements for automatic resource lookup - LocalizationHelper for C# runtime string localization - Windows auto-selects language based on OS locale Any language can now be added by contributing a new .resw file. Addresses #40
1 parent bc140e9 commit afaaa04

16 files changed

Lines changed: 1105 additions & 120 deletions

src/OpenClaw.Tray.WinUI/App.xaml.cs

Lines changed: 29 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -400,7 +400,7 @@ private MenuFlyout BuildTrayMenuFlyout()
400400
// Sessions
401401
if (_lastSessions.Length > 0)
402402
{
403-
var sessionsMenu = new MenuFlyoutSubItem { Text = $"📋 Sessions ({_lastSessions.Length})" };
403+
var sessionsMenu = new MenuFlyoutSubItem { Text = $"📋 {string.Format(LocalizationHelper.GetString("Menu_SessionsFormat"), _lastSessions.Length)}" };
404404
foreach (var session in _lastSessions.Take(5))
405405
{
406406
var sessionItem = new MenuFlyoutItem { Text = session.DisplayText };
@@ -733,7 +733,7 @@ private void BuildTrayMenuPopup(TrayMenuWindow menu)
733733

734734
// Status
735735
var statusIcon = MenuDisplayHelper.GetStatusIcon(_currentStatus);
736-
menu.AddMenuItem($"Status: {_currentStatus}", statusIcon, "status");
736+
menu.AddMenuItem(string.Format(LocalizationHelper.GetString("Menu_StatusFormat"), LocalizationHelper.GetConnectionStatusText(_currentStatus)), statusIcon, "status");
737737

738738
// Activity (if any)
739739
if (_currentActivity != null && _currentActivity.Kind != OpenClaw.Shared.ActivityKind.Idle)
@@ -745,14 +745,14 @@ private void BuildTrayMenuPopup(TrayMenuWindow menu)
745745
if (_lastUsage != null || _lastUsageStatus != null || _lastUsageCost != null)
746746
{
747747
var usageText = _lastUsage?.DisplayText;
748-
if (string.IsNullOrWhiteSpace(usageText) || string.Equals(usageText, "No usage data", StringComparison.Ordinal))
748+
if (string.IsNullOrWhiteSpace(usageText) || string.Equals(usageText, "No usage data", StringComparison.Ordinal) || string.Equals(usageText, LocalizationHelper.GetString("Menu_NoUsageData"), StringComparison.Ordinal))
749749
{
750750
usageText = _lastUsageStatus?.Providers.Count > 0
751751
? MenuDisplayHelper.FormatProviderSummary(_lastUsageStatus.Providers.Count)
752-
: "No usage data";
752+
: LocalizationHelper.GetString("Menu_NoUsageData");
753753
}
754754

755-
menu.AddMenuItem(usageText ?? "No usage data", "📊", "activity:usage");
755+
menu.AddMenuItem(usageText ?? LocalizationHelper.GetString("Menu_NoUsageData"), "📊", "activity:usage");
756756

757757
if (!string.IsNullOrWhiteSpace(_lastUsage?.ProviderSummary))
758758
{
@@ -793,28 +793,28 @@ private void BuildTrayMenuPopup(TrayMenuWindow menu)
793793

794794
if (_nodeService.IsPendingApproval)
795795
{
796-
menu.AddMenuItem("⏳ Waiting for approval...", "", "", isEnabled: false, indent: true);
796+
menu.AddMenuItem(LocalizationHelper.GetString("Menu_NodeWaitingApproval"), "", "", isEnabled: false, indent: true);
797797
menu.AddMenuItem($"ID: {_nodeService.ShortDeviceId}...", "", "copydeviceid", indent: true);
798798
}
799799
else if (_nodeService.IsPaired && _nodeService.IsConnected)
800800
{
801-
menu.AddMenuItem("✅ Paired & Connected", "", "", isEnabled: false, indent: true);
801+
menu.AddMenuItem(LocalizationHelper.GetString("Menu_NodePairedConnected"), "", "", isEnabled: false, indent: true);
802802
}
803803
else if (_nodeService.IsConnected)
804804
{
805-
menu.AddMenuItem("🔄 Connecting...", "", "", isEnabled: false, indent: true);
805+
menu.AddMenuItem(LocalizationHelper.GetString("Menu_NodeConnecting"), "", "", isEnabled: false, indent: true);
806806
}
807807
else
808808
{
809-
menu.AddMenuItem("⚪ Disconnected", "", "", isEnabled: false, indent: true);
809+
menu.AddMenuItem(LocalizationHelper.GetString("Menu_NodeDisconnected"), "", "", isEnabled: false, indent: true);
810810
}
811811
}
812812

813813
// Sessions (if any) - show meaningful info like the WinForms version
814814
if (_lastSessions.Length > 0)
815815
{
816816
menu.AddSeparator();
817-
menu.AddMenuItem($"Sessions ({_lastSessions.Length})", "💬", "activity:sessions");
817+
menu.AddMenuItem(string.Format(LocalizationHelper.GetString("Menu_SessionsFormat"), _lastSessions.Length), "💬", "activity:sessions");
818818

819819
var visibleSessions = _lastSessions.Take(3).ToArray();
820820
foreach (var session in visibleSessions)
@@ -858,10 +858,10 @@ private void BuildTrayMenuPopup(TrayMenuWindow menu)
858858
"📝",
859859
$"session-verbose|{nextVerbose}|{session.Key}",
860860
indent: true);
861-
menu.AddMenuItem("↳ Reset session", "♻️", $"session-reset|{session.Key}", indent: true);
862-
menu.AddMenuItem("↳ Compact log", "🗜️", $"session-compact|{session.Key}", indent: true);
861+
menu.AddMenuItem(LocalizationHelper.GetString("Menu_ResetSession"), "♻️", $"session-reset|{session.Key}", indent: true);
862+
menu.AddMenuItem(LocalizationHelper.GetString("Menu_CompactLog"), "🗜️", $"session-compact|{session.Key}", indent: true);
863863
if (!session.IsMain && !string.Equals(session.Key, "global", StringComparison.OrdinalIgnoreCase))
864-
menu.AddMenuItem("↳ Delete session", "🗑️", $"session-delete|{session.Key}", indent: true);
864+
menu.AddMenuItem(LocalizationHelper.GetString("Menu_DeleteSession"), "🗑️", $"session-delete|{session.Key}", indent: true);
865865
}
866866
if (_lastSessions.Length > visibleSessions.Length)
867867
menu.AddMenuItem($"+{_lastSessions.Length - visibleSessions.Length} more...", "", "", isEnabled: false, indent: true);
@@ -885,7 +885,7 @@ private void BuildTrayMenuPopup(TrayMenuWindow menu)
885885
if (_lastNodes.Length > 0)
886886
{
887887
menu.AddSeparator();
888-
menu.AddMenuItem($"Nodes ({_lastNodes.Length})", "🖥️", "activity:nodes");
888+
menu.AddMenuItem(string.Format(LocalizationHelper.GetString("Menu_NodesFormat"), _lastNodes.Length), "🖥️", "activity:nodes");
889889

890890
var visibleNodes = _lastNodes.Take(3).ToArray();
891891
foreach (var node in visibleNodes)
@@ -898,15 +898,15 @@ private void BuildTrayMenuPopup(TrayMenuWindow menu)
898898
if (_lastNodes.Length > visibleNodes.Length)
899899
menu.AddMenuItem($"+{_lastNodes.Length - visibleNodes.Length} more...", "", "", isEnabled: false, indent: true);
900900

901-
menu.AddMenuItem("Copy node summary", "📋", "copynodesummary", indent: true);
901+
menu.AddMenuItem(LocalizationHelper.GetString("Menu_CopyNodeSummary"), "📋", "copynodesummary", indent: true);
902902
}
903903

904904
var recentActivity = GetRecentActivity(maxItems: 4);
905905
if (recentActivity.Count > 0)
906906
{
907907
menu.AddSeparator();
908908
var totalActivity = ActivityStreamService.GetItems().Count;
909-
menu.AddMenuItem($"Recent Activity ({totalActivity})", "⚡", "activity");
909+
menu.AddMenuItem(string.Format(LocalizationHelper.GetString("Menu_RecentActivityFormat"), totalActivity), "⚡", "activity");
910910
foreach (var line in recentActivity)
911911
{
912912
menu.AddMenuItem(TruncateMenuText(line, 94), "", "", isEnabled: false, indent: true);
@@ -916,24 +916,26 @@ private void BuildTrayMenuPopup(TrayMenuWindow menu)
916916
menu.AddSeparator();
917917

918918
// Actions
919-
menu.AddMenuItem("Open Dashboard", "🌐", "dashboard");
920-
menu.AddMenuItem("Open Web Chat", "💬", "webchat");
921-
menu.AddMenuItem("Quick Send...", "📤", "quicksend");
922-
menu.AddMenuItem("Activity Stream...", "⚡", "activity");
923-
menu.AddMenuItem("Notification History...", "📋", "history");
924-
menu.AddMenuItem("Run Health Check", "🔄", "healthcheck");
919+
menu.AddMenuItem(LocalizationHelper.GetString("Menu_OpenDashboard"), "🌐", "dashboard");
920+
menu.AddMenuItem(LocalizationHelper.GetString("Menu_OpenWebChat"), "💬", "webchat");
921+
menu.AddMenuItem(LocalizationHelper.GetString("Menu_QuickSend"), "📤", "quicksend");
922+
menu.AddMenuItem(LocalizationHelper.GetString("Menu_ActivityStream"), "⚡", "activity");
923+
menu.AddMenuItem(LocalizationHelper.GetString("Menu_NotificationHistory"), "📋", "history");
924+
menu.AddMenuItem(LocalizationHelper.GetString("Menu_RunHealthCheck"), "🔄", "healthcheck");
925925

926926
menu.AddSeparator();
927927

928928
// Settings
929-
menu.AddMenuItem("Settings...", "⚙️", "settings");
930-
var autoStartText = (_settings?.AutoStart ?? false) ? "Auto-start ✓" : "Auto-start";
929+
menu.AddMenuItem(LocalizationHelper.GetString("Menu_Settings"), "⚙️", "settings");
930+
var autoStartText = (_settings?.AutoStart ?? false)
931+
? LocalizationHelper.GetString("Menu_AutoStartEnabled")
932+
: LocalizationHelper.GetString("Menu_AutoStart");
931933
menu.AddMenuItem(autoStartText, "🚀", "autostart");
932934

933935
menu.AddSeparator();
934936

935-
menu.AddMenuItem("Open Log File", "📄", "log");
936-
menu.AddMenuItem("Exit", "❌", "exit");
937+
menu.AddMenuItem(LocalizationHelper.GetString("Menu_OpenLogFile"), "📄", "log");
938+
menu.AddMenuItem(LocalizationHelper.GetString("Menu_Exit"), "❌", "exit");
937939
}
938940

939941
// Keep the old MenuFlyout method for reference but it won't be used
@@ -985,7 +987,7 @@ private void BuildTrayMenu(MenuFlyout flyout)
985987
flyout.Items.Add(new MenuFlyoutSeparator());
986988
var sessionsHeader = new MenuFlyoutItem
987989
{
988-
Text = $"💬 Sessions ({_lastSessions.Length})"
990+
Text = $"💬 {string.Format(LocalizationHelper.GetString("Menu_SessionsFormat"), _lastSessions.Length)}"
989991
};
990992
sessionsHeader.Click += (s, e) => OpenDashboard("sessions");
991993
flyout.Items.Add(sessionsHeader);

src/OpenClaw.Tray.WinUI/Dialogs/QuickSendDialog.cs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public QuickSendDialog(OpenClawGatewayClient client, string? prefillMessage = nu
5151
_client = client;
5252

5353
// Window setup
54-
Title = "Settings — OpenClaw Tray";
54+
Title = LocalizationHelper.GetString("WindowTitle_QuickSend");
5555
this.SetWindowSize(400, 200);
5656
this.CenterOnScreen();
5757
this.SetIcon(IconHelper.GetStatusIconPath(ConnectionStatus.Connected));
@@ -73,14 +73,14 @@ public QuickSendDialog(OpenClawGatewayClient client, string? prefillMessage = nu
7373

7474
var header = new TextBlock
7575
{
76-
Text = "📤 Quick Send",
76+
Text = LocalizationHelper.GetString("QuickSend_Header"),
7777
Style = (Style)Application.Current.Resources["SubtitleTextBlockStyle"]
7878
};
7979
root.Children.Add(header);
8080

8181
_messageTextBox = new TextBox
8282
{
83-
PlaceholderText = "Type your message...",
83+
PlaceholderText = LocalizationHelper.GetString("QuickSend_Placeholder"),
8484
AcceptsReturn = false,
8585
Text = prefillMessage ?? ""
8686
};
@@ -101,13 +101,13 @@ public QuickSendDialog(OpenClawGatewayClient client, string? prefillMessage = nu
101101
};
102102
buttonPanel.Children.Add(_statusText);
103103

104-
var cancelButton = new Button { Content = "Cancel" };
104+
var cancelButton = new Button { Content = LocalizationHelper.GetString("QuickSend_CancelButton") };
105105
cancelButton.Click += (s, e) => Close();
106106
buttonPanel.Children.Add(cancelButton);
107107

108108
_sendButton = new Button
109109
{
110-
Content = "Send",
110+
Content = LocalizationHelper.GetString("QuickSend_SendButton"),
111111
Style = (Style)Application.Current.Resources["AccentButtonStyle"]
112112
};
113113
_sendButton.Click += OnSendClick;
@@ -169,22 +169,22 @@ private async Task SendMessageAsync()
169169
_isSending = true;
170170
_sendButton.IsEnabled = false;
171171
_messageTextBox.IsEnabled = false;
172-
_statusText.Text = "Sending...";
172+
_statusText.Text = LocalizationHelper.GetString("QuickSend_Sending");
173173

174174
try
175175
{
176176
await _client.SendChatMessageAsync(message);
177177
Logger.Info($"Quick send: {message}");
178178
new ToastContentBuilder()
179-
.AddText("Message Sent")
180-
.AddText("Your message was sent to OpenClaw.")
179+
.AddText(LocalizationHelper.GetString("QuickSend_ToastTitle"))
180+
.AddText(LocalizationHelper.GetString("QuickSend_ToastBody"))
181181
.Show();
182182
Close();
183183
}
184184
catch (Exception ex)
185185
{
186186
Logger.Error($"Quick send failed: {ex.Message}");
187-
_statusText.Text = "❌ Failed";
187+
_statusText.Text = LocalizationHelper.GetString("QuickSend_Failed");
188188
_sendButton.IsEnabled = true;
189189
_messageTextBox.IsEnabled = true;
190190
_isSending = false;

src/OpenClaw.Tray.WinUI/Dialogs/UpdateDialog.cs

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Microsoft.UI.Xaml;
22
using Microsoft.UI.Xaml.Controls;
33
using Microsoft.UI.Xaml.Media;
4+
using OpenClawTray.Helpers;
45
using System;
56
using System.Threading.Tasks;
67
using WinUIEx;
@@ -25,7 +26,7 @@ public sealed class UpdateDialog : WindowEx
2526

2627
public UpdateDialog(string version, string changelog)
2728
{
28-
Title = "OpenClaw Update";
29+
Title = LocalizationHelper.GetString("WindowTitle_Update");
2930
this.SetWindowSize(560, 420);
3031
this.CenterOnScreen();
3132
this.SetIcon("Assets\\openclaw.ico");
@@ -43,7 +44,7 @@ public UpdateDialog(string version, string changelog)
4344
// Header
4445
var header = new TextBlock
4546
{
46-
Text = $"🎉 Version {version} is available!",
47+
Text = string.Format(LocalizationHelper.GetString("Update_VersionAvailable"), version),
4748
Style = (Style)Application.Current.Resources["SubtitleTextBlockStyle"]
4849
};
4950
Grid.SetRow(header, 0);
@@ -55,13 +56,13 @@ public UpdateDialog(string version, string changelog)
5556
var currentVersion = typeof(UpdateDialog).Assembly.GetName().Version?.ToString() ?? "Unknown";
5657
content.Children.Add(new TextBlock
5758
{
58-
Text = $"Current version: {currentVersion}",
59+
Text = string.Format(LocalizationHelper.GetString("Update_CurrentVersion"), currentVersion),
5960
Foreground = (Brush)Application.Current.Resources["TextFillColorSecondaryBrush"]
6061
});
6162

6263
content.Children.Add(new TextBlock
6364
{
64-
Text = "What's New:",
65+
Text = LocalizationHelper.GetString("Update_WhatsNew"),
6566
FontWeight = Microsoft.UI.Text.FontWeights.SemiBold
6667
});
6768

@@ -86,17 +87,17 @@ public UpdateDialog(string version, string changelog)
8687
Spacing = 8
8788
};
8889

89-
var skipButton = new Button { Content = "Skip This Version" };
90+
var skipButton = new Button { Content = LocalizationHelper.GetString("Update_SkipButton") };
9091
skipButton.Click += (s, e) => { _result = UpdateDialogResult.Skip; Close(); };
9192
buttonPanel.Children.Add(skipButton);
9293

93-
var laterButton = new Button { Content = "Remind Me Later" };
94+
var laterButton = new Button { Content = LocalizationHelper.GetString("Update_RemindLaterButton") };
9495
laterButton.Click += (s, e) => { _result = UpdateDialogResult.RemindLater; Close(); };
9596
buttonPanel.Children.Add(laterButton);
9697

9798
var downloadButton = new Button
9899
{
99-
Content = "Download & Install",
100+
Content = LocalizationHelper.GetString("Update_DownloadButton"),
100101
Style = (Style)Application.Current.Resources["AccentButtonStyle"]
101102
};
102103
downloadButton.Click += (s, e) => { _result = UpdateDialogResult.Download; Close(); };

src/OpenClaw.Tray.WinUI/Dialogs/WelcomeDialog.cs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Microsoft.UI.Xaml;
22
using Microsoft.UI.Xaml.Controls;
33
using Microsoft.UI.Xaml.Media;
4+
using OpenClawTray.Helpers;
45
using System;
56
using System.Threading.Tasks;
67
using WinUIEx;
@@ -17,7 +18,7 @@ public sealed class WelcomeDialog : WindowEx
1718

1819
public WelcomeDialog()
1920
{
20-
Title = "Welcome to OpenClaw";
21+
Title = LocalizationHelper.GetString("WindowTitle_Welcome");
2122
this.SetWindowSize(480, 440);
2223
this.CenterOnScreen();
2324
this.SetIcon("Assets\\openclaw.ico");
@@ -49,7 +50,7 @@ public WelcomeDialog()
4950
});
5051
headerPanel.Children.Add(new TextBlock
5152
{
52-
Text = "Welcome to OpenClaw!",
53+
Text = LocalizationHelper.GetString("Welcome_Title"),
5354
Style = (Style)Application.Current.Resources["TitleTextBlockStyle"],
5455
VerticalAlignment = VerticalAlignment.Center
5556
});
@@ -61,26 +62,26 @@ public WelcomeDialog()
6162

6263
content.Children.Add(new TextBlock
6364
{
64-
Text = "OpenClaw Tray is your Windows companion for OpenClaw, the AI-powered personal assistant.",
65+
Text = LocalizationHelper.GetString("Welcome_Description"),
6566
TextWrapping = TextWrapping.Wrap
6667
});
6768

6869
var gettingStarted = new StackPanel { Spacing = 8 };
6970
gettingStarted.Children.Add(new TextBlock
7071
{
71-
Text = "To get started, you'll need:",
72+
Text = LocalizationHelper.GetString("Welcome_GettingStarted"),
7273
FontWeight = Microsoft.UI.Text.FontWeights.SemiBold
7374
});
7475

7576
var bulletList = new StackPanel { Spacing = 4, Margin = new Thickness(16, 0, 0, 0) };
76-
bulletList.Children.Add(new TextBlock { Text = "• A running OpenClaw gateway" });
77-
bulletList.Children.Add(new TextBlock { Text = "• Your API token from the dashboard" });
77+
bulletList.Children.Add(new TextBlock { Text = LocalizationHelper.GetString("Welcome_NeedGateway") });
78+
bulletList.Children.Add(new TextBlock { Text = LocalizationHelper.GetString("Welcome_NeedToken") });
7879
gettingStarted.Children.Add(bulletList);
7980
content.Children.Add(gettingStarted);
8081

8182
var docsButton = new HyperlinkButton
8283
{
83-
Content = "📚 View Documentation",
84+
Content = LocalizationHelper.GetString("Welcome_ViewDocs"),
8485
NavigateUri = new Uri("https://docs.molt.bot/web/dashboard")
8586
};
8687
content.Children.Add(docsButton);
@@ -96,7 +97,7 @@ public WelcomeDialog()
9697
Spacing = 8
9798
};
9899

99-
var laterButton = new Button { Content = "Later" };
100+
var laterButton = new Button { Content = LocalizationHelper.GetString("Welcome_LaterButton") };
100101
laterButton.Click += (s, e) =>
101102
{
102103
_result = ContentDialogResult.None;
@@ -106,7 +107,7 @@ public WelcomeDialog()
106107

107108
var settingsButton = new Button
108109
{
109-
Content = "Open Settings",
110+
Content = LocalizationHelper.GetString("Welcome_OpenSettingsButton"),
110111
Style = (Style)Application.Current.Resources["AccentButtonStyle"]
111112
};
112113
settingsButton.Click += (s, e) =>
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using Microsoft.Windows.ApplicationModel.Resources;
2+
using OpenClaw.Shared;
3+
4+
namespace OpenClawTray.Helpers;
5+
6+
public static class LocalizationHelper
7+
{
8+
private static ResourceLoader? _loader;
9+
10+
private static ResourceLoader Loader => _loader ??= new ResourceLoader();
11+
12+
public static string GetString(string resourceKey)
13+
{
14+
try
15+
{
16+
var value = Loader.GetString(resourceKey);
17+
return string.IsNullOrEmpty(value) ? resourceKey : value;
18+
}
19+
catch
20+
{
21+
return resourceKey;
22+
}
23+
}
24+
25+
public static string GetConnectionStatusText(ConnectionStatus status) => status switch
26+
{
27+
ConnectionStatus.Connected => GetString("StatusDisplay_Connected"),
28+
ConnectionStatus.Connecting => GetString("StatusDisplay_Connecting"),
29+
ConnectionStatus.Disconnected => GetString("StatusDisplay_Disconnected"),
30+
ConnectionStatus.Error => GetString("StatusDisplay_Error"),
31+
_ => GetString("StatusDisplay_Unknown")
32+
};
33+
}

0 commit comments

Comments
 (0)