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
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:
parent
c8e55fe194
commit
c85d4e7571
98
docs/LOCALIZATION.md
Normal file
98
docs/LOCALIZATION.md
Normal 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)"
|
||||
```
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 { }
|
||||
|
||||
@ -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 <server></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>
|
||||
|
||||
@ -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)
|
||||
• 或通过隧道连接到 localhost:ssh -N -L 18789:localhost:18789 <服务器></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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
");
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user