Skip to content

Commit c85d4e7

Browse files
shanselmanCopilot
andauthored
feat: complete Chinese localization + contributor guide (#60)
Localize ~40 remaining hardcoded English strings (toasts, canvas, webchat, download dialog). Both en-US and zh-CN now have 163 resource keys, fully in sync. - Add LocalizationHelper.SetLanguageOverride() for unpackaged app locale testing - Add docs/LOCALIZATION.md contributor guide - File issue #61 calling for community translations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent c8e55fe commit c85d4e7

11 files changed

Lines changed: 496 additions & 62 deletions

File tree

docs/LOCALIZATION.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Localization Guide
2+
3+
OpenClaw Tray uses WinUI `.resw` resource files for localization. Windows automatically selects the correct language based on the OS locale — no user configuration needed.
4+
5+
## Currently Supported Languages
6+
7+
| Language | Locale | Resource File |
8+
|----------|--------|---------------|
9+
| English (US) | `en-us` | `Strings/en-us/Resources.resw` |
10+
| Chinese (Simplified) | `zh-cn` | `Strings/zh-cn/Resources.resw` |
11+
12+
## Adding a New Language
13+
14+
1. **Copy the English resource file** as your starting point:
15+
16+
```
17+
src/OpenClaw.Tray.WinUI/Strings/en-us/Resources.resw
18+
```
19+
20+
2. **Create a new folder** for your locale under `Strings/`:
21+
22+
```
23+
src/OpenClaw.Tray.WinUI/Strings/<locale>/Resources.resw
24+
```
25+
26+
Use the standard BCP-47 locale tag in lowercase (e.g., `de-de`, `fr-fr`, `ja-jp`, `ko-kr`, `pt-br`, `es-es`).
27+
28+
3. **Translate the `<value>` elements** — do not change the `name` attributes. Each entry looks like:
29+
30+
```xml
31+
<data name="SettingsSaveButton.Content" xml:space="preserve">
32+
<value>Save</value> <!-- ← translate this -->
33+
</data>
34+
```
35+
36+
4. **Keep format placeholders intact.** Some strings use `{0}`, `{1}`, etc. These must remain in the translation:
37+
38+
```xml
39+
<data name="Menu_SessionsFormat" xml:space="preserve">
40+
<value>Sessions ({0})</value> <!-- {0} = session count -->
41+
</data>
42+
```
43+
44+
5. **Do not translate resource key names** (the `name` attribute). Only translate `<value>` content.
45+
46+
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.
47+
48+
## How It Works
49+
50+
### XAML strings (automatic)
51+
Elements with `x:Uid` attributes are automatically matched to resource keys:
52+
```xml
53+
<Button x:Uid="SettingsSaveButton" Content="Save" />
54+
```
55+
Maps to resource key `SettingsSaveButton.Content`.
56+
57+
### C# runtime strings (via LocalizationHelper)
58+
Code uses `LocalizationHelper.GetString("key")` to load strings at runtime:
59+
```csharp
60+
Title = LocalizationHelper.GetString("WindowTitle_Settings");
61+
```
62+
63+
### Language selection
64+
Windows picks the language automatically based on the user's OS display language. No in-app language picker is needed.
65+
66+
## Testing a Language Locally
67+
68+
To test a specific locale without changing your Windows language:
69+
70+
1. Open `src/OpenClaw.Tray.WinUI/App.xaml.cs`
71+
2. Add this line at the top of the `App()` constructor, **before** `InitializeComponent()`:
72+
```csharp
73+
LocalizationHelper.SetLanguageOverride("zh-CN");
74+
```
75+
3. Build and run (`dotnet build src/OpenClaw.Tray.WinUI -r win-x64`). Remove the line when done testing.
76+
77+
> **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.
78+
79+
## Resource Key Naming Conventions
80+
81+
| Pattern | Used For | Example |
82+
|---------|----------|---------|
83+
| `ComponentName.Property` | XAML `x:Uid` bindings | `SettingsSaveButton.Content` |
84+
| `WindowTitle_Name` | Window title bars | `WindowTitle_Settings` |
85+
| `Toast_Name` | Toast notification text | `Toast_NodePaired` |
86+
| `Menu_Name` | Tray menu items | `Menu_Settings` |
87+
| `Status_Name` | Status display text | `Status_Connected` |
88+
| `TimeAgo_Format` | Relative time strings | `TimeAgo_MinutesFormat` |
89+
90+
## Validation
91+
92+
Both resource files must have the **same set of keys**. You can verify with:
93+
94+
```powershell
95+
$en = (Select-String -Path "src\OpenClaw.Tray.WinUI\Strings\en-us\Resources.resw" -Pattern '<data name="' | Measure-Object).Count
96+
$new = (Select-String -Path "src\OpenClaw.Tray.WinUI\Strings\<locale>\Resources.resw" -Pattern '<data name="' | Measure-Object).Count
97+
Write-Host "en-us: $en keys | <locale>: $new keys | Match: $($en -eq $new)"
98+
```

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

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -569,8 +569,8 @@ private void CopyDeviceIdToClipboard()
569569

570570
// Show toast confirming copy
571571
new ToastContentBuilder()
572-
.AddText("📋 Device ID Copied")
573-
.AddText($"Run: openclaw devices approve {_nodeService.ShortDeviceId}...")
572+
.AddText(LocalizationHelper.GetString("Toast_DeviceIdCopied"))
573+
.AddText(string.Format(LocalizationHelper.GetString("Toast_DeviceIdCopiedDetail"), _nodeService.ShortDeviceId))
574574
.Show();
575575
}
576576
catch (Exception ex)
@@ -598,8 +598,8 @@ private void CopyNodeSummaryToClipboard()
598598
global::Windows.ApplicationModel.DataTransfer.Clipboard.SetContent(dataPackage);
599599

600600
new ToastContentBuilder()
601-
.AddText("📋 Node summary copied")
602-
.AddText($"{_lastNodes.Length} node(s) copied to clipboard")
601+
.AddText(LocalizationHelper.GetString("Toast_NodeSummaryCopied"))
602+
.AddText(string.Format(LocalizationHelper.GetString("Toast_NodeSummaryCopiedDetail"), _lastNodes.Length))
603603
.Show();
604604
}
605605
catch (Exception ex)
@@ -655,8 +655,8 @@ private async Task ExecuteSessionActionAsync(string action, string sessionKey, s
655655
if (!sent)
656656
{
657657
new ToastContentBuilder()
658-
.AddText("❌ Session action failed")
659-
.AddText("Could not send request to gateway.")
658+
.AddText(LocalizationHelper.GetString("Toast_SessionActionFailed"))
659+
.AddText(LocalizationHelper.GetString("Toast_SessionActionFailedDetail"))
660660
.Show();
661661
return;
662662
}
@@ -672,7 +672,7 @@ private async Task ExecuteSessionActionAsync(string action, string sessionKey, s
672672
try
673673
{
674674
new ToastContentBuilder()
675-
.AddText("❌ Session action failed")
675+
.AddText(LocalizationHelper.GetString("Toast_SessionActionFailed"))
676676
.AddText(ex.Message)
677677
.Show();
678678
}
@@ -1158,8 +1158,8 @@ private void OnNodeStatusChanged(object? sender, ConnectionStatus status)
11581158
try
11591159
{
11601160
new ToastContentBuilder()
1161-
.AddText("🔌 Node Mode Active")
1162-
.AddText("This PC can now receive commands from the agent (canvas, screenshots)")
1161+
.AddText(LocalizationHelper.GetString("Toast_NodeModeActive"))
1162+
.AddText(LocalizationHelper.GetString("Toast_NodeModeActiveDetail"))
11631163
.Show();
11641164
}
11651165
catch { /* ignore */ }
@@ -1177,16 +1177,16 @@ private void OnPairingStatusChanged(object? sender, OpenClaw.Shared.PairingStatu
11771177
AddRecentActivity("Node pairing pending", category: "node", dashboardPath: "nodes", nodeId: args.DeviceId);
11781178
// Show toast with approval instructions
11791179
new ToastContentBuilder()
1180-
.AddText("⏳ Awaiting Pairing Approval")
1181-
.AddText($"Run on gateway: openclaw devices approve {args.DeviceId.Substring(0, 16)}...")
1180+
.AddText(LocalizationHelper.GetString("Toast_PairingPending"))
1181+
.AddText(string.Format(LocalizationHelper.GetString("Toast_PairingPendingDetail"), args.DeviceId.Substring(0, 16)))
11821182
.Show();
11831183
}
11841184
else if (args.Status == OpenClaw.Shared.PairingStatus.Paired)
11851185
{
11861186
AddRecentActivity("Node paired", category: "node", dashboardPath: "nodes", nodeId: args.DeviceId);
11871187
new ToastContentBuilder()
1188-
.AddText("✅ Node Paired!")
1189-
.AddText("This PC can now receive commands from the agent")
1188+
.AddText(LocalizationHelper.GetString("Toast_NodePaired"))
1189+
.AddText(LocalizationHelper.GetString("Toast_NodePairedDetail"))
11901190
.Show();
11911191
}
11921192
}
@@ -1499,8 +1499,8 @@ private async Task RunHealthCheckAsync(bool userInitiated = false)
14991499
if (userInitiated)
15001500
{
15011501
new ToastContentBuilder()
1502-
.AddText("Health Check")
1503-
.AddText("Gateway is not connected yet.")
1502+
.AddText(LocalizationHelper.GetString("Toast_HealthCheck"))
1503+
.AddText(LocalizationHelper.GetString("Toast_HealthCheckNotConnected"))
15041504
.Show();
15051505
}
15061506
return;
@@ -1513,8 +1513,8 @@ private async Task RunHealthCheckAsync(bool userInitiated = false)
15131513
if (userInitiated)
15141514
{
15151515
new ToastContentBuilder()
1516-
.AddText("Health Check")
1517-
.AddText("Health check request sent.")
1516+
.AddText(LocalizationHelper.GetString("Toast_HealthCheck"))
1517+
.AddText(LocalizationHelper.GetString("Toast_HealthCheckSent"))
15181518
.Show();
15191519
}
15201520
}
@@ -1524,7 +1524,7 @@ private async Task RunHealthCheckAsync(bool userInitiated = false)
15241524
if (userInitiated)
15251525
{
15261526
new ToastContentBuilder()
1527-
.AddText("Health Check Failed")
1527+
.AddText(LocalizationHelper.GetString("Toast_HealthCheckFailed"))
15281528
.AddText(ex.Message)
15291529
.Show();
15301530
}
@@ -1744,10 +1744,10 @@ private void ShowSurfaceImprovementsTipIfNeeded()
17441744
try
17451745
{
17461746
new ToastContentBuilder()
1747-
.AddText("⚡ New: Activity Stream")
1748-
.AddText("Open the tray menu to view live sessions, usage, and node activity in one flyout.")
1747+
.AddText(LocalizationHelper.GetString("Toast_ActivityStreamTip"))
1748+
.AddText(LocalizationHelper.GetString("Toast_ActivityStreamTipDetail"))
17491749
.AddButton(new ToastButton()
1750-
.SetContent("Open Activity Stream")
1750+
.SetContent(LocalizationHelper.GetString("Toast_ActivityStreamTipButton"))
17511751
.AddArgument("action", "open_activity"))
17521752
.Show();
17531753
}

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

Lines changed: 3 additions & 2 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 Updatum;
56

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

1819
public void ShowAsync()
1920
{
20-
_window = new Window { Title = "Downloading Update..." };
21+
_window = new Window { Title = LocalizationHelper.GetString("WindowTitle_Downloading") };
2122
_window.SystemBackdrop = new MicaBackdrop();
2223

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

2728
panel.Children.Add(progressText);

src/OpenClaw.Tray.WinUI/Helpers/LocalizationHelper.cs

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,41 @@ namespace OpenClawTray.Helpers;
55

66
public static class LocalizationHelper
77
{
8-
private static ResourceLoader? _loader;
8+
private static ResourceManager? _resourceManager;
9+
private static ResourceContext? _overrideContext;
10+
private static string? _languageOverride;
911

10-
private static ResourceLoader Loader => _loader ??= new ResourceLoader();
12+
/// <summary>
13+
/// Force a specific language for testing (e.g. "zh-CN").
14+
/// Must be called before any GetString calls.
15+
/// </summary>
16+
public static void SetLanguageOverride(string language)
17+
{
18+
_languageOverride = language;
19+
_resourceManager = null;
20+
_overrideContext = null;
21+
}
22+
23+
private static ResourceManager Manager => _resourceManager ??= new ResourceManager();
24+
25+
private static ResourceContext GetContext()
26+
{
27+
if (_overrideContext != null) return _overrideContext;
28+
if (_languageOverride != null)
29+
{
30+
_overrideContext = Manager.CreateResourceContext();
31+
_overrideContext.QualifierValues["Language"] = _languageOverride;
32+
return _overrideContext;
33+
}
34+
return Manager.CreateResourceContext();
35+
}
1136

1237
public static string GetString(string resourceKey)
1338
{
1439
try
1540
{
16-
var value = Loader.GetString(resourceKey);
41+
var candidate = Manager.MainResourceMap.GetValue($"Resources/{resourceKey}", GetContext());
42+
var value = candidate?.ValueAsString;
1743
return string.IsNullOrEmpty(value) ? resourceKey : value;
1844
}
1945
catch

src/OpenClaw.Tray.WinUI/Services/NodeService.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Microsoft.UI.Dispatching;
55
using OpenClaw.Shared;
66
using OpenClaw.Shared.Capabilities;
7+
using OpenClawTray.Helpers;
78
using OpenClawTray.Windows;
89
using Microsoft.UI.Xaml;
910

@@ -417,8 +418,8 @@ private async Task<ScreenCaptureResult> OnScreenCapture(ScreenCaptureArgs args)
417418
try
418419
{
419420
new ToastContentBuilder()
420-
.AddText("📸 Screen Captured")
421-
.AddText("OpenClaw agent captured your screen")
421+
.AddText(LocalizationHelper.GetString("Toast_ScreenCaptured"))
422+
.AddText(LocalizationHelper.GetString("Toast_ScreenCapturedDetail"))
422423
.Show();
423424
}
424425
catch { /* ignore notification errors */ }
@@ -457,8 +458,8 @@ private async Task<CameraSnapResult> OnCameraSnap(CameraSnapArgs args)
457458
try
458459
{
459460
new ToastContentBuilder()
460-
.AddText("📷 Camera access blocked")
461-
.AddText("Enable camera access in Windows Privacy settings for OpenClaw Tray")
461+
.AddText(LocalizationHelper.GetString("Toast_CameraBlocked"))
462+
.AddText(LocalizationHelper.GetString("Toast_CameraBlockedDetail"))
462463
.Show();
463464
}
464465
catch { }

0 commit comments

Comments
 (0)