feat: complete Chinese localization + contributor guide (#60)
Some checks failed
Build and Test / test (push) Has been cancelled
Build and Test / build (win-arm64) (push) Has been cancelled
Build and Test / build (win-x64) (push) Has been cancelled
Build and Test / build-msix (ARM64, win-arm64) (push) Has been cancelled
Build and Test / build-msix (x64, win-x64) (push) Has been cancelled
Build and Test / build-extension (arm64) (push) Has been cancelled
Build and Test / build-extension (x64) (push) Has been cancelled
Build and Test / release (push) Has been cancelled

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>
This commit is contained in:
Scott Hanselman 2026-03-17 21:12:45 -07:00 committed by GitHub
parent c8e55fe194
commit c85d4e7571
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 496 additions and 62 deletions

98
docs/LOCALIZATION.md Normal file
View File

@ -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)"
```

View File

@ -569,8 +569,8 @@ public partial class App : Application
// 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)
@ -598,8 +598,8 @@ public partial class App : Application
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)
@ -655,8 +655,8 @@ public partial class App : Application
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;
}
@ -672,7 +672,7 @@ public partial class App : Application
try
{
new ToastContentBuilder()
.AddText("❌ Session action failed")
.AddText(LocalizationHelper.GetString("Toast_SessionActionFailed"))
.AddText(ex.Message)
.Show();
}
@ -1158,8 +1158,8 @@ public partial class App : Application
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 */ }
@ -1177,16 +1177,16 @@ public partial class App : Application
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();
}
}
@ -1499,8 +1499,8 @@ public partial class App : Application
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;
@ -1513,8 +1513,8 @@ public partial class App : Application
if (userInitiated)
{
new ToastContentBuilder()
.AddText("Health Check")
.AddText("Health check request sent.")
.AddText(LocalizationHelper.GetString("Toast_HealthCheck"))
.AddText(LocalizationHelper.GetString("Toast_HealthCheckSent"))
.Show();
}
}
@ -1524,7 +1524,7 @@ public partial class App : Application
if (userInitiated)
{
new ToastContentBuilder()
.AddText("Health Check Failed")
.AddText(LocalizationHelper.GetString("Toast_HealthCheckFailed"))
.AddText(ex.Message)
.Show();
}
@ -1744,10 +1744,10 @@ public partial class App : Application
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();
}

View File

@ -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;
@ -17,11 +18,11 @@ public sealed class DownloadProgressDialog
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);

View File

@ -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

View File

@ -4,6 +4,7 @@ using Microsoft.Toolkit.Uwp.Notifications;
using Microsoft.UI.Dispatching;
using OpenClaw.Shared;
using OpenClaw.Shared.Capabilities;
using OpenClawTray.Helpers;
using OpenClawTray.Windows;
using Microsoft.UI.Xaml;
@ -417,8 +418,8 @@ public class NodeService : IDisposable
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 */ }
@ -457,8 +458,8 @@ public class NodeService : IDisposable
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 { }

View File

@ -466,4 +466,165 @@
<value>n/a</value>
</data>
<!-- ==================== CanvasWindow.xaml ==================== -->
<data name="WindowTitle_Canvas" xml:space="preserve">
<value>Canvas</value>
</data>
<data name="CanvasErrorTitle.Text" xml:space="preserve">
<value>❌ Canvas Error</value>
</data>
<data name="CanvasRetryButton.Content" xml:space="preserve">
<value>Retry</value>
</data>
<data name="Canvas_ReadyTitle" xml:space="preserve">
<value>🎨 Canvas Ready</value>
</data>
<data name="Canvas_WaitingForContent" xml:space="preserve">
<value>Waiting for content...</value>
</data>
<!-- ==================== WebChatWindow.xaml ==================== -->
<data name="WebChatErrorTitle.Text" xml:space="preserve">
<value>Web Chat Unavailable</value>
</data>
<data name="WebChatOpenBrowserButton.Content" xml:space="preserve">
<value>Open in Browser Instead</value>
</data>
<data name="WebChat_ConnectionError" xml:space="preserve">
<value>Can't reach OpenClaw Gateway</value>
</data>
<data name="WebChat_ConnectionErrorDetail" xml:space="preserve">
<value>The gateway at {0} is not responding.
To connect:
• Make sure your OpenClaw gateway is running
• If remote, connect via VPN to your home network
• Or use SSH tunnel: ssh -N -L 18789:localhost:18789 your-server</value>
</data>
<data name="WebChat_CertError" xml:space="preserve">
<value>The gateway HTTPS certificate is not trusted.
To connect securely:
• Use an HTTPS gateway URL (for example: https://host.tailnet.ts.net)
• If self-signed, import the cert into Windows Trusted Root Certification Authorities
• Or use SSH tunnel to localhost and keep using localhost URLs</value>
</data>
<data name="WebChat_InvalidUrl" xml:space="preserve">
<value>Invalid gateway URL: {0}</value>
</data>
<data name="WebChat_SecureContextRequired" xml:space="preserve">
<value>Web chat requires a secure context.
There is no safe bypass for remote plain HTTP: browsers and WebView enforce this.
Use one of these options:
• Use a trusted HTTPS/WSS endpoint (Let's Encrypt, Tailscale Serve, Caddy)
• If self-signed, import your gateway CA/cert into Windows Trusted Root (certmgr.msc)
• Or tunnel to localhost: ssh -N -L 18789:localhost:18789 &lt;server&gt;</value>
</data>
<!-- ==================== TrayMenuWindow.xaml ==================== -->
<data name="WindowTitle_TrayMenu" xml:space="preserve">
<value>OpenClaw Menu</value>
</data>
<!-- ==================== Toast: Device / Node ==================== -->
<data name="Toast_DeviceIdCopied" xml:space="preserve">
<value>📋 Device ID Copied</value>
</data>
<data name="Toast_DeviceIdCopiedDetail" xml:space="preserve">
<value>Run: openclaw devices approve {0}...</value>
</data>
<data name="Toast_NodeSummaryCopied" xml:space="preserve">
<value>📋 Node summary copied</value>
</data>
<data name="Toast_NodeSummaryCopiedDetail" xml:space="preserve">
<value>{0} node(s) copied to clipboard</value>
</data>
<!-- ==================== Toast: Session ==================== -->
<data name="Toast_SessionActionFailed" xml:space="preserve">
<value>❌ Session action failed</value>
</data>
<data name="Toast_SessionActionFailedDetail" xml:space="preserve">
<value>Could not send request to gateway.</value>
</data>
<!-- ==================== Toast: Node Mode ==================== -->
<data name="Toast_NodeModeActive" xml:space="preserve">
<value>🔌 Node Mode Active</value>
</data>
<data name="Toast_NodeModeActiveDetail" xml:space="preserve">
<value>This PC can now receive commands from the agent (canvas, screenshots)</value>
</data>
<data name="Toast_PairingPending" xml:space="preserve">
<value>⏳ Awaiting Pairing Approval</value>
</data>
<data name="Toast_PairingPendingDetail" xml:space="preserve">
<value>Run on gateway: openclaw devices approve {0}...</value>
</data>
<data name="Toast_NodePaired" xml:space="preserve">
<value>✅ Node Paired!</value>
</data>
<data name="Toast_NodePairedDetail" xml:space="preserve">
<value>This PC can now receive commands from the agent</value>
</data>
<!-- ==================== Toast: Health Check ==================== -->
<data name="Toast_HealthCheck" xml:space="preserve">
<value>Health Check</value>
</data>
<data name="Toast_HealthCheckNotConnected" xml:space="preserve">
<value>Gateway is not connected yet.</value>
</data>
<data name="Toast_HealthCheckSent" xml:space="preserve">
<value>Health check request sent.</value>
</data>
<data name="Toast_HealthCheckFailed" xml:space="preserve">
<value>Health Check Failed</value>
</data>
<!-- ==================== Toast: Screen / Camera ==================== -->
<data name="Toast_ScreenCaptured" xml:space="preserve">
<value>📸 Screen Captured</value>
</data>
<data name="Toast_ScreenCapturedDetail" xml:space="preserve">
<value>OpenClaw agent captured your screen</value>
</data>
<data name="Toast_CameraBlocked" xml:space="preserve">
<value>📷 Camera access blocked</value>
</data>
<data name="Toast_CameraBlockedDetail" xml:space="preserve">
<value>Enable camera access in Windows Privacy settings for OpenClaw Tray</value>
</data>
<!-- ==================== Toast: Activity Stream Tip ==================== -->
<data name="Toast_ActivityStreamTip" xml:space="preserve">
<value>⚡ New: Activity Stream</value>
</data>
<data name="Toast_ActivityStreamTipDetail" xml:space="preserve">
<value>Open the tray menu to view live sessions, usage, and node activity in one flyout.</value>
</data>
<data name="Toast_ActivityStreamTipButton" xml:space="preserve">
<value>Open Activity Stream</value>
</data>
<!-- ==================== DownloadProgressDialog ==================== -->
<data name="WindowTitle_Downloading" xml:space="preserve">
<value>Downloading Update...</value>
</data>
<data name="Download_ProgressText" xml:space="preserve">
<value>Downloading update...</value>
</data>
</root>

View File

@ -466,4 +466,165 @@
<value>无</value>
</data>
<!-- ==================== CanvasWindow.xaml ==================== -->
<data name="WindowTitle_Canvas" xml:space="preserve">
<value>画布</value>
</data>
<data name="CanvasErrorTitle.Text" xml:space="preserve">
<value>❌ 画布错误</value>
</data>
<data name="CanvasRetryButton.Content" xml:space="preserve">
<value>重试</value>
</data>
<data name="Canvas_ReadyTitle" xml:space="preserve">
<value>🎨 画布就绪</value>
</data>
<data name="Canvas_WaitingForContent" xml:space="preserve">
<value>等待内容...</value>
</data>
<!-- ==================== WebChatWindow.xaml ==================== -->
<data name="WebChatErrorTitle.Text" xml:space="preserve">
<value>网页聊天不可用</value>
</data>
<data name="WebChatOpenBrowserButton.Content" xml:space="preserve">
<value>在浏览器中打开</value>
</data>
<data name="WebChat_ConnectionError" xml:space="preserve">
<value>无法连接到 OpenClaw 网关</value>
</data>
<data name="WebChat_ConnectionErrorDetail" xml:space="preserve">
<value>网关 {0} 没有响应。
连接方法:
• 确保 OpenClaw 网关正在运行
• 如果是远程网关,请通过 VPN 连接到您的家庭网络
• 或使用 SSH 隧道ssh -N -L 18789:localhost:18789 your-server</value>
</data>
<data name="WebChat_CertError" xml:space="preserve">
<value>网关 HTTPS 证书不受信任。
安全连接方法:
• 使用 HTTPS 网关地址例如https://host.tailnet.ts.net
• 如果是自签名证书,请将其导入 Windows 受信任的根证书颁发机构
• 或使用 SSH 隧道连接到 localhost 并继续使用 localhost 地址</value>
</data>
<data name="WebChat_InvalidUrl" xml:space="preserve">
<value>无效的网关地址: {0}</value>
</data>
<data name="WebChat_SecureContextRequired" xml:space="preserve">
<value>网页聊天需要安全上下文。
远程纯 HTTP 没有安全的绕过方法:浏览器和 WebView 强制执行此限制。
请使用以下选项之一:
• 使用受信任的 HTTPS/WSS 端点Let's Encrypt、Tailscale Serve、Caddy
• 如果是自签名证书,请将网关 CA/证书导入 Windows 受信任的根证书certmgr.msc
• 或通过隧道连接到 localhostssh -N -L 18789:localhost:18789 &lt;服务器&gt;</value>
</data>
<!-- ==================== TrayMenuWindow.xaml ==================== -->
<data name="WindowTitle_TrayMenu" xml:space="preserve">
<value>OpenClaw 菜单</value>
</data>
<!-- ==================== Toast: Device / Node ==================== -->
<data name="Toast_DeviceIdCopied" xml:space="preserve">
<value>📋 设备 ID 已复制</value>
</data>
<data name="Toast_DeviceIdCopiedDetail" xml:space="preserve">
<value>运行: openclaw devices approve {0}...</value>
</data>
<data name="Toast_NodeSummaryCopied" xml:space="preserve">
<value>📋 节点摘要已复制</value>
</data>
<data name="Toast_NodeSummaryCopiedDetail" xml:space="preserve">
<value>已复制 {0} 个节点到剪贴板</value>
</data>
<!-- ==================== Toast: Session ==================== -->
<data name="Toast_SessionActionFailed" xml:space="preserve">
<value>❌ 会话操作失败</value>
</data>
<data name="Toast_SessionActionFailedDetail" xml:space="preserve">
<value>无法向网关发送请求。</value>
</data>
<!-- ==================== Toast: Node Mode ==================== -->
<data name="Toast_NodeModeActive" xml:space="preserve">
<value>🔌 节点模式已激活</value>
</data>
<data name="Toast_NodeModeActiveDetail" xml:space="preserve">
<value>此电脑现在可以接收来自代理的命令(画布、截图)</value>
</data>
<data name="Toast_PairingPending" xml:space="preserve">
<value>⏳ 等待配对批准</value>
</data>
<data name="Toast_PairingPendingDetail" xml:space="preserve">
<value>在网关上运行: openclaw devices approve {0}...</value>
</data>
<data name="Toast_NodePaired" xml:space="preserve">
<value>✅ 节点已配对!</value>
</data>
<data name="Toast_NodePairedDetail" xml:space="preserve">
<value>此电脑现在可以接收来自代理的命令</value>
</data>
<!-- ==================== Toast: Health Check ==================== -->
<data name="Toast_HealthCheck" xml:space="preserve">
<value>健康检查</value>
</data>
<data name="Toast_HealthCheckNotConnected" xml:space="preserve">
<value>网关尚未连接。</value>
</data>
<data name="Toast_HealthCheckSent" xml:space="preserve">
<value>健康检查请求已发送。</value>
</data>
<data name="Toast_HealthCheckFailed" xml:space="preserve">
<value>健康检查失败</value>
</data>
<!-- ==================== Toast: Screen / Camera ==================== -->
<data name="Toast_ScreenCaptured" xml:space="preserve">
<value>📸 屏幕已捕获</value>
</data>
<data name="Toast_ScreenCapturedDetail" xml:space="preserve">
<value>OpenClaw 代理捕获了您的屏幕</value>
</data>
<data name="Toast_CameraBlocked" xml:space="preserve">
<value>📷 相机访问被阻止</value>
</data>
<data name="Toast_CameraBlockedDetail" xml:space="preserve">
<value>请在 Windows 隐私设置中为 OpenClaw Tray 启用相机访问</value>
</data>
<!-- ==================== Toast: Activity Stream Tip ==================== -->
<data name="Toast_ActivityStreamTip" xml:space="preserve">
<value>⚡ 新功能: 活动流</value>
</data>
<data name="Toast_ActivityStreamTipDetail" xml:space="preserve">
<value>打开托盘菜单即可查看实时会话、用量和节点活动。</value>
</data>
<data name="Toast_ActivityStreamTipButton" xml:space="preserve">
<value>打开活动流</value>
</data>
<!-- ==================== DownloadProgressDialog ==================== -->
<data name="WindowTitle_Downloading" xml:space="preserve">
<value>正在下载更新...</value>
</data>
<data name="Download_ProgressText" xml:space="preserve">
<value>正在下载更新...</value>
</data>
</root>

View File

@ -31,7 +31,7 @@
VerticalAlignment="Center"
Spacing="16"
Padding="32">
<TextBlock Text="❌ Canvas Error"
<TextBlock x:Uid="CanvasErrorTitle" Text="❌ Canvas Error"
FontSize="18"
FontWeight="SemiBold"
HorizontalAlignment="Center"/>
@ -40,7 +40,7 @@
MaxWidth="400"
HorizontalAlignment="Center"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
<Button Content="Retry"
<Button x:Uid="CanvasRetryButton" Content="Retry"
HorizontalAlignment="Center"
Click="OnRetryClick"/>
</StackPanel>

View File

@ -4,6 +4,7 @@ using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.UI.Xaml;
using Microsoft.Web.WebView2.Core;
using OpenClawTray.Helpers;
using OpenClawTray.Services;
using WinUIEx;
using Windows.Storage.Streams;
@ -124,26 +125,26 @@ public sealed partial class CanvasWindow : WindowEx
else
{
// Default blank page with styling
CanvasWebView.CoreWebView2.NavigateToString(@"
CanvasWebView.CoreWebView2.NavigateToString($@"
<!DOCTYPE html>
<html>
<head>
<style>
body {
body {{
margin: 0;
padding: 20px;
font-family: 'Segoe UI', sans-serif;
background: transparent;
color: #333;
}
@media (prefers-color-scheme: dark) {
body { color: #eee; }
}
}}
@media (prefers-color-scheme: dark) {{
body {{ color: #eee; }}
}}
</style>
</head>
<body>
<h2>🎨 Canvas Ready</h2>
<p>Waiting for content...</p>
<h2>{LocalizationHelper.GetString("Canvas_ReadyTitle")}</h2>
<p>{LocalizationHelper.GetString("Canvas_WaitingForContent")}</p>
</body>
</html>
");

View File

@ -52,11 +52,11 @@
<!-- Error display (hidden by default) -->
<ScrollViewer x:Name="ErrorPanel" Grid.Row="1" Visibility="Collapsed" Padding="20">
<StackPanel Spacing="12">
<TextBlock Text="Web Chat Unavailable" FontSize="18" FontWeight="SemiBold"
<TextBlock x:Uid="WebChatErrorTitle" Text="Web Chat Unavailable" FontSize="18" FontWeight="SemiBold"
Foreground="{ThemeResource SystemFillColorCriticalBrush}"/>
<TextBlock x:Name="ErrorText" TextWrapping="Wrap" IsTextSelectionEnabled="True"
FontFamily="Consolas" FontSize="12"/>
<Button Content="Open in Browser Instead" Click="OnPopout" HorizontalAlignment="Left"/>
<Button x:Uid="WebChatOpenBrowserButton" Content="Open in Browser Instead" Click="OnPopout" HorizontalAlignment="Left"/>
</StackPanel>
</ScrollViewer>
</Grid>

View File

@ -99,12 +99,8 @@ public sealed partial class WebChatWindow : WindowEx
e.WebErrorStatus == CoreWebView2WebErrorStatus.ServerUnreachable))
{
Logger.Info("WebChatWindow: Gateway unreachable, showing friendly error");
ShowErrorMessage("Can't reach OpenClaw Gateway\n\n" +
$"The gateway at {_gatewayUrl} is not responding.\n\n" +
"To connect:\n" +
"• Make sure your OpenClaw gateway is running\n" +
"• If remote, connect via VPN to your home network\n" +
"• Or use SSH tunnel: ssh -N -L 18789:localhost:18789 your-server");
ShowErrorMessage(LocalizationHelper.GetString("WebChat_ConnectionError") + "\n\n" +
string.Format(LocalizationHelper.GetString("WebChat_ConnectionErrorDetail"), _gatewayUrl));
return;
}
@ -112,12 +108,7 @@ public sealed partial class WebChatWindow : WindowEx
e.WebErrorStatus.ToString().Contains("Certificate", StringComparison.OrdinalIgnoreCase))
{
Logger.Info("WebChatWindow: TLS certificate issue detected");
ShowErrorMessage(
"The gateway HTTPS certificate is not trusted.\n\n" +
"To connect securely:\n" +
"• Use an HTTPS gateway URL (for example: https://host.tailnet.ts.net)\n" +
"• If self-signed, import the cert into Windows Trusted Root Certification Authorities\n" +
"• Or use SSH tunnel to localhost and keep using localhost URLs");
ShowErrorMessage(LocalizationHelper.GetString("WebChat_CertError"));
}
};
WebView.CoreWebView2.NavigationCompleted += _navigationCompletedHandler;
@ -184,7 +175,7 @@ public sealed partial class WebChatWindow : WindowEx
if (!GatewayUrlHelper.TryNormalizeWebSocketUrl(_gatewayUrl, out var normalizedGatewayUrl) ||
!Uri.TryCreate(normalizedGatewayUrl, UriKind.Absolute, out var gatewayUri))
{
errorMessage = $"Invalid gateway URL: {_gatewayUrl}";
errorMessage = string.Format(LocalizationHelper.GetString("WebChat_InvalidUrl"), _gatewayUrl);
return false;
}
@ -194,13 +185,7 @@ public sealed partial class WebChatWindow : WindowEx
if (webScheme == "http" && !IsLocalHost(gatewayUri))
{
errorMessage =
"Web chat requires a secure context.\n\n" +
"There is no safe bypass for remote plain HTTP: browsers and WebView enforce this.\n\n" +
"Use one of these options:\n" +
"• Use a trusted HTTPS/WSS endpoint (Let's Encrypt, Tailscale Serve, Caddy)\n" +
"• If self-signed, import your gateway CA/cert into Windows Trusted Root (certmgr.msc)\n" +
"• Or tunnel to localhost: ssh -N -L 18789:localhost:18789 <mac>";
errorMessage = LocalizationHelper.GetString("WebChat_SecureContextRequired");
return false;
}