Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions docs/LOCALIZATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Localization Guide

OpenClaw Tray uses WinUI `.resw` resource files for localization. Windows automatically selects the correct language based on the OS locale — no user configuration needed.

## Currently Supported Languages

| Language | Locale | Resource File |
|----------|--------|---------------|
| English (US) | `en-us` | `Strings/en-us/Resources.resw` |
| Chinese (Simplified) | `zh-cn` | `Strings/zh-cn/Resources.resw` |

## Adding a New Language

1. **Copy the English resource file** as your starting point:

```
src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw
```

2. **Create a new folder** for your locale under `Strings/`:

```
src/OpenClaw.Tray.WinUI/Strings/<locale>/Resources.resw
```

Use the standard BCP-47 locale tag in lowercase (e.g., `de-de`, `fr-fr`, `ja-jp`, `ko-kr`, `pt-br`, `es-es`).

3. **Translate the `<value>` elements** — do not change the `name` attributes. Each entry looks like:

```xml
<data name="SettingsSaveButton.Content" xml:space="preserve">
<value>Save</value> <!-- ← translate this -->
</data>
```

4. **Keep format placeholders intact.** Some strings use `{0}`, `{1}`, etc. These must remain in the translation:

```xml
<data name="Menu_SessionsFormat" xml:space="preserve">
<value>Sessions ({0})</value> <!-- {0} = session count -->
</data>
```

5. **Do not translate resource key names** (the `name` attribute). Only translate `<value>` content.

6. **Submit a pull request** with just your new `Resources.resw` file. No code changes are needed — the build system automatically discovers new locale folders.

## How It Works

### XAML strings (automatic)
Elements with `x:Uid` attributes are automatically matched to resource keys:
```xml
<Button x:Uid="SettingsSaveButton" Content="Save" />
```
Maps to resource key `SettingsSaveButton.Content`.

### C# runtime strings (via LocalizationHelper)
Code uses `LocalizationHelper.GetString("key")` to load strings at runtime:
```csharp
Title = LocalizationHelper.GetString("WindowTitle_Settings");
```

### Language selection
Windows picks the language automatically based on the user's OS display language. No in-app language picker is needed.

## Testing a Language Locally

To test a specific locale without changing your Windows language:

1. Open `src/OpenClaw.Tray.WinUI/App.xaml.cs`
2. Add this line at the top of the `App()` constructor, **before** `InitializeComponent()`:
```csharp
LocalizationHelper.SetLanguageOverride("zh-CN");
```
3. Build and run (`dotnet build src/OpenClaw.Tray.WinUI -r win-x64`). Remove the line when done testing.

> **Note:** This overrides `LocalizationHelper.GetString()` calls (menus, toasts, dialogs, window titles). XAML `x:Uid` bindings follow the OS display language. For full XAML localization testing, change your Windows display language in Settings → Time & Language.

## Resource Key Naming Conventions

| Pattern | Used For | Example |
|---------|----------|---------|
| `ComponentName.Property` | XAML `x:Uid` bindings | `SettingsSaveButton.Content` |
| `WindowTitle_Name` | Window title bars | `WindowTitle_Settings` |
| `Toast_Name` | Toast notification text | `Toast_NodePaired` |
| `Menu_Name` | Tray menu items | `Menu_Settings` |
| `Status_Name` | Status display text | `Status_Connected` |
| `TimeAgo_Format` | Relative time strings | `TimeAgo_MinutesFormat` |

## Validation

Both resource files must have the **same set of keys**. You can verify with:

```powershell
$en = (Select-String -Path "src\OpenClaw.Tray.WinUI\Strings\en-us\Resources.resw" -Pattern '<data name="' | Measure-Object).Count
$new = (Select-String -Path "src\OpenClaw.Tray.WinUI\Strings\<locale>\Resources.resw" -Pattern '<data name="' | Measure-Object).Count
Write-Host "en-us: $en keys | <locale>: $new keys | Match: $($en -eq $new)"
```
42 changes: 21 additions & 21 deletions src/OpenClaw.Tray.WinUI/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@
StartDeepLinkServer();

// Register global hotkey if enabled
if (_settings.GlobalHotkeyEnabled)

Check warning on line 284 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / test

Dereference of a possibly null reference.

Check warning on line 284 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / test

Dereference of a possibly null reference.

Check warning on line 284 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-arm64)

Dereference of a possibly null reference.

Check warning on line 284 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-arm64)

Dereference of a possibly null reference.

Check warning on line 284 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-arm64)

Dereference of a possibly null reference.

Check warning on line 284 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-x64)

Dereference of a possibly null reference.

Check warning on line 284 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-x64)

Dereference of a possibly null reference.

Check warning on line 284 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build (win-x64)

Dereference of a possibly null reference.

Check warning on line 284 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build-msix (win-arm64)

Dereference of a possibly null reference.

Check warning on line 284 in src/OpenClaw.Tray.WinUI/App.xaml.cs

View workflow job for this annotation

GitHub Actions / build-msix (win-x64)

Dereference of a possibly null reference.
{
_globalHotkey = new GlobalHotkeyService();
_globalHotkey.HotkeyPressed += OnGlobalHotkeyPressed;
Expand Down Expand Up @@ -569,8 +569,8 @@

// Show toast confirming copy
new ToastContentBuilder()
.AddText("📋 Device ID Copied")
.AddText($"Run: openclaw devices approve {_nodeService.ShortDeviceId}...")
.AddText(LocalizationHelper.GetString("Toast_DeviceIdCopied"))
.AddText(string.Format(LocalizationHelper.GetString("Toast_DeviceIdCopiedDetail"), _nodeService.ShortDeviceId))
.Show();
}
catch (Exception ex)
Expand Down Expand Up @@ -598,8 +598,8 @@
global::Windows.ApplicationModel.DataTransfer.Clipboard.SetContent(dataPackage);

new ToastContentBuilder()
.AddText("📋 Node summary copied")
.AddText($"{_lastNodes.Length} node(s) copied to clipboard")
.AddText(LocalizationHelper.GetString("Toast_NodeSummaryCopied"))
.AddText(string.Format(LocalizationHelper.GetString("Toast_NodeSummaryCopiedDetail"), _lastNodes.Length))
.Show();
}
catch (Exception ex)
Expand Down Expand Up @@ -655,8 +655,8 @@
if (!sent)
{
new ToastContentBuilder()
.AddText("❌ Session action failed")
.AddText("Could not send request to gateway.")
.AddText(LocalizationHelper.GetString("Toast_SessionActionFailed"))
.AddText(LocalizationHelper.GetString("Toast_SessionActionFailedDetail"))
.Show();
return;
}
Expand All @@ -672,7 +672,7 @@
try
{
new ToastContentBuilder()
.AddText("❌ Session action failed")
.AddText(LocalizationHelper.GetString("Toast_SessionActionFailed"))
.AddText(ex.Message)
.Show();
}
Expand Down Expand Up @@ -1158,8 +1158,8 @@
try
{
new ToastContentBuilder()
.AddText("🔌 Node Mode Active")
.AddText("This PC can now receive commands from the agent (canvas, screenshots)")
.AddText(LocalizationHelper.GetString("Toast_NodeModeActive"))
.AddText(LocalizationHelper.GetString("Toast_NodeModeActiveDetail"))
.Show();
}
catch { /* ignore */ }
Expand All @@ -1177,16 +1177,16 @@
AddRecentActivity("Node pairing pending", category: "node", dashboardPath: "nodes", nodeId: args.DeviceId);
// Show toast with approval instructions
new ToastContentBuilder()
.AddText("⏳ Awaiting Pairing Approval")
.AddText($"Run on gateway: openclaw devices approve {args.DeviceId.Substring(0, 16)}...")
.AddText(LocalizationHelper.GetString("Toast_PairingPending"))
.AddText(string.Format(LocalizationHelper.GetString("Toast_PairingPendingDetail"), args.DeviceId.Substring(0, 16)))
.Show();
}
else if (args.Status == OpenClaw.Shared.PairingStatus.Paired)
{
AddRecentActivity("Node paired", category: "node", dashboardPath: "nodes", nodeId: args.DeviceId);
new ToastContentBuilder()
.AddText("✅ Node Paired!")
.AddText("This PC can now receive commands from the agent")
.AddText(LocalizationHelper.GetString("Toast_NodePaired"))
.AddText(LocalizationHelper.GetString("Toast_NodePairedDetail"))
.Show();
}
}
Expand Down Expand Up @@ -1499,8 +1499,8 @@
if (userInitiated)
{
new ToastContentBuilder()
.AddText("Health Check")
.AddText("Gateway is not connected yet.")
.AddText(LocalizationHelper.GetString("Toast_HealthCheck"))
.AddText(LocalizationHelper.GetString("Toast_HealthCheckNotConnected"))
.Show();
}
return;
Expand All @@ -1513,8 +1513,8 @@
if (userInitiated)
{
new ToastContentBuilder()
.AddText("Health Check")
.AddText("Health check request sent.")
.AddText(LocalizationHelper.GetString("Toast_HealthCheck"))
.AddText(LocalizationHelper.GetString("Toast_HealthCheckSent"))
.Show();
}
}
Expand All @@ -1524,7 +1524,7 @@
if (userInitiated)
{
new ToastContentBuilder()
.AddText("Health Check Failed")
.AddText(LocalizationHelper.GetString("Toast_HealthCheckFailed"))
.AddText(ex.Message)
.Show();
}
Expand Down Expand Up @@ -1744,10 +1744,10 @@
try
{
new ToastContentBuilder()
.AddText("⚡ New: Activity Stream")
.AddText("Open the tray menu to view live sessions, usage, and node activity in one flyout.")
.AddText(LocalizationHelper.GetString("Toast_ActivityStreamTip"))
.AddText(LocalizationHelper.GetString("Toast_ActivityStreamTipDetail"))
.AddButton(new ToastButton()
.SetContent("Open Activity Stream")
.SetContent(LocalizationHelper.GetString("Toast_ActivityStreamTipButton"))
.AddArgument("action", "open_activity"))
.Show();
}
Expand Down
5 changes: 3 additions & 2 deletions src/OpenClaw.Tray.WinUI/Dialogs/DownloadProgressDialog.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using OpenClawTray.Helpers;
using Updatum;

namespace OpenClawTray.Dialogs;
Expand All @@ -17,11 +18,11 @@ public DownloadProgressDialog(UpdatumManager updater)

public void ShowAsync()
{
_window = new Window { Title = "Downloading Update..." };
_window = new Window { Title = LocalizationHelper.GetString("WindowTitle_Downloading") };
_window.SystemBackdrop = new MicaBackdrop();

var panel = new StackPanel { Padding = new Thickness(20) };
var progressText = new TextBlock { Text = "Downloading update...", Margin = new Thickness(0, 0, 0, 10) };
var progressText = new TextBlock { Text = LocalizationHelper.GetString("Download_ProgressText"), Margin = new Thickness(0, 0, 0, 10) };
var progressBar = new ProgressBar { IsIndeterminate = true };

panel.Children.Add(progressText);
Expand Down
32 changes: 29 additions & 3 deletions src/OpenClaw.Tray.WinUI/Helpers/LocalizationHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,41 @@ namespace OpenClawTray.Helpers;

public static class LocalizationHelper
{
private static ResourceLoader? _loader;
private static ResourceManager? _resourceManager;
private static ResourceContext? _overrideContext;
private static string? _languageOverride;

private static ResourceLoader Loader => _loader ??= new ResourceLoader();
/// <summary>
/// Force a specific language for testing (e.g. "zh-CN").
/// Must be called before any GetString calls.
/// </summary>
public static void SetLanguageOverride(string language)
{
_languageOverride = language;
_resourceManager = null;
_overrideContext = null;
}

private static ResourceManager Manager => _resourceManager ??= new ResourceManager();

private static ResourceContext GetContext()
{
if (_overrideContext != null) return _overrideContext;
if (_languageOverride != null)
{
_overrideContext = Manager.CreateResourceContext();
_overrideContext.QualifierValues["Language"] = _languageOverride;
return _overrideContext;
}
return Manager.CreateResourceContext();
}

public static string GetString(string resourceKey)
{
try
{
var value = Loader.GetString(resourceKey);
var candidate = Manager.MainResourceMap.GetValue($"Resources/{resourceKey}", GetContext());
var value = candidate?.ValueAsString;
return string.IsNullOrEmpty(value) ? resourceKey : value;
}
catch
Expand Down
9 changes: 5 additions & 4 deletions src/OpenClaw.Tray.WinUI/Services/NodeService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Microsoft.UI.Dispatching;
using OpenClaw.Shared;
using OpenClaw.Shared.Capabilities;
using OpenClawTray.Helpers;
using OpenClawTray.Windows;
using Microsoft.UI.Xaml;

Expand Down Expand Up @@ -417,8 +418,8 @@ private async Task<ScreenCaptureResult> OnScreenCapture(ScreenCaptureArgs args)
try
{
new ToastContentBuilder()
.AddText("📸 Screen Captured")
.AddText("OpenClaw agent captured your screen")
.AddText(LocalizationHelper.GetString("Toast_ScreenCaptured"))
.AddText(LocalizationHelper.GetString("Toast_ScreenCapturedDetail"))
.Show();
}
catch { /* ignore notification errors */ }
Expand Down Expand Up @@ -457,8 +458,8 @@ private async Task<CameraSnapResult> OnCameraSnap(CameraSnapArgs args)
try
{
new ToastContentBuilder()
.AddText("📷 Camera access blocked")
.AddText("Enable camera access in Windows Privacy settings for OpenClaw Tray")
.AddText(LocalizationHelper.GetString("Toast_CameraBlocked"))
.AddText(LocalizationHelper.GetString("Toast_CameraBlockedDetail"))
.Show();
}
catch { }
Expand Down
Loading
Loading