using System.Text.RegularExpressions;
using System.Xml.Linq;
namespace OpenClaw.Tray.Tests;
///
/// Validates that all localization resource files are consistent with en-us.
/// Catches missing/extra keys and broken format placeholders early — before translation PRs land.
///
public class LocalizationValidationTests
{
private static readonly HashSet InvariantOrDeferredResourceKeys = new(StringComparer.Ordinal)
{
"AboutPage_TextBlock_19.Text",
"CanvasWindow_TextBlock_31.Text",
"CanvasWindow_winexWindowEx_2.Title",
"ChatWindow_winexWindowEx_2.Title",
"HubWindow_winexWindowEx_2.Title",
"TitleText.Text",
"TokenPromptBox.Header",
"TokenTextBox.Header",
"TrayMenuWindow_winexWindowEx_2.Title",
"Onboarding_Connection_QrButton",
"Onboarding_Connection_Token",
"WindowTitle_TrayMenu",
"WindowTitle_Update",
// STT/TTS card invariants — these are protocol/brand identifiers
// not user-visible prose. They intentionally read the same in every
// locale: "eleven_multilingual_v2" is an ElevenLabs model
// identifier, "ElevenLabs" is a brand name.
// VoiceOverlayWindow window-title key — matches the convention
// for ChatWindow / HubWindow / CanvasWindow / TrayMenuWindow.
"VoiceOverlayWindow_winexWindowEx_2.Title",
"CapabilitiesPage_TtsElevenLabsModel.PlaceholderText",
"CapabilitiesPage_TtsProviderElevenLabs.Content",
// Sample IDs / brand identifiers — same across locales.
"VoiceSettingsPage_ElevenLabsVoiceIdBox.PlaceholderText",
"VoiceSettingsPage_ElevenLabsModelBox.PlaceholderText",
};
private static readonly string[] RequiredRuntimeOnboardingKeys =
[
"Onboarding_Ready_Node_ScreenCapture",
"Onboarding_Ready_Node_ScreenCapture_Sub",
"Onboarding_Ready_Node_Camera",
"Onboarding_Ready_Node_Camera_Sub",
"Onboarding_Ready_Node_SystemCmd",
"Onboarding_Ready_Node_SystemCmd_Sub",
"Onboarding_Ready_Node_Canvas",
"Onboarding_Ready_Node_Canvas_Sub",
"Onboarding_Ready_Node_Notify",
"Onboarding_Ready_Node_Notify_Sub",
];
private static string GetRepositoryRoot()
{
var envRepoRoot = Environment.GetEnvironmentVariable("OPENCLAW_REPO_ROOT");
if (!string.IsNullOrWhiteSpace(envRepoRoot) && Directory.Exists(envRepoRoot))
return envRepoRoot;
var directory = new DirectoryInfo(AppContext.BaseDirectory);
while (directory != null)
{
if (Directory.Exists(Path.Combine(directory.FullName, ".git")) &&
File.Exists(Path.Combine(directory.FullName, "README.md")))
return directory.FullName;
directory = directory.Parent;
}
throw new InvalidOperationException(
"Could not find repository root. Set OPENCLAW_REPO_ROOT to the repo path.");
}
private static string GetStringsDirectory() =>
Path.Combine(GetRepositoryRoot(), "src", "OpenClaw.Tray.WinUI", "Strings");
private static Dictionary LoadResw(string path)
{
var doc = XDocument.Load(path);
return doc.Descendants("data")
.ToDictionary(
e => e.Attribute("name")!.Value,
e => e.Element("value")?.Value ?? string.Empty);
}
private static List GetNonEnglishLocaleDirectories(string stringsDir) =>
Directory.GetDirectories(stringsDir)
.Where(d => !string.Equals(Path.GetFileName(d), "en-us", StringComparison.OrdinalIgnoreCase))
.OrderBy(Path.GetFileName, StringComparer.Ordinal)
.ToList();
private static bool IsInvariantValue(string value) =>
string.IsNullOrWhiteSpace(value) ||
Regex.IsMatch(value, @"^\d+(\.\d+)?[Kk]?$", RegexOptions.CultureInvariant) ||
Regex.IsMatch(value, @"^(v\d|[A-Z0-9._%+-]+://|~?/|\.NET|WinUI|WinAppSDK|OpenClaw$|GitHub|MCP|JSON|API|HTTP|HTTPS|SSH|TLS|WebView2|OAuth|QR$|Cron$|main$|user$|machine-name$)", RegexOptions.CultureInvariant) ||
value.Contains("openclaw://", StringComparison.Ordinal) ||
value.Contains("github.com", StringComparison.Ordinal) ||
value.Contains("openclaw.ai", StringComparison.Ordinal) ||
value.Contains("localhost", StringComparison.Ordinal) ||
value.Contains("ws://", StringComparison.Ordinal) ||
value.Contains("wss://", StringComparison.Ordinal) ||
value.Contains("http://", StringComparison.Ordinal) ||
value.Contains("https://", StringComparison.Ordinal) ||
value.Contains("~/", StringComparison.Ordinal);
private static bool IsInvariantOrDeferred(string key, string value) =>
InvariantOrDeferredResourceKeys.Contains(key) || IsInvariantValue(value);
[Fact]
public void AllLocales_HaveExactlySameKeysAsEnUs()
{
var stringsDir = GetStringsDirectory();
var referencePath = Path.Combine(stringsDir, "en-us", "Resources.resw");
Assert.True(File.Exists(referencePath), $"Reference file not found: {referencePath}");
var referenceKeys = LoadResw(referencePath).Keys.ToHashSet(StringComparer.Ordinal);
var localeDirs = GetNonEnglishLocaleDirectories(stringsDir);
Assert.NotEmpty(localeDirs);
foreach (var localeDir in localeDirs)
{
var locale = Path.GetFileName(localeDir);
var reswPath = Path.Combine(localeDir, "Resources.resw");
Assert.True(File.Exists(reswPath), $"Expected Resources.resw for locale '{locale}'.");
var localeKeys = LoadResw(reswPath).Keys.ToHashSet(StringComparer.Ordinal);
var missing = referenceKeys.Except(localeKeys).OrderBy(k => k).ToList();
var extra = localeKeys.Except(referenceKeys).OrderBy(k => k).ToList();
Assert.True(missing.Count == 0,
$"Locale '{locale}' is missing {missing.Count} key(s): {string.Join(", ", missing.Take(10))}");
Assert.True(extra.Count == 0,
$"Locale '{locale}' has {extra.Count} unexpected key(s): {string.Join(", ", extra.Take(10))}");
}
}
[Fact]
public void AllLocales_PreserveFormatPlaceholders()
{
var stringsDir = GetStringsDirectory();
var referenceResw = LoadResw(Path.Combine(stringsDir, "en-us", "Resources.resw"));
var keysWithPlaceholders = referenceResw
.Where(kv => Regex.IsMatch(kv.Value, @"\{\d+\}"))
.ToList();
if (keysWithPlaceholders.Count == 0)
return;
var localeDirs = GetNonEnglishLocaleDirectories(stringsDir);
foreach (var localeDir in localeDirs)
{
var locale = Path.GetFileName(localeDir);
var reswPath = Path.Combine(localeDir, "Resources.resw");
if (!File.Exists(reswPath)) continue;
var localeResw = LoadResw(reswPath);
foreach (var (key, enValue) in keysWithPlaceholders)
{
if (!localeResw.TryGetValue(key, out var localeValue))
continue;
var enPlaceholders = Regex.Matches(enValue, @"\{\d+\}")
.Select(m => m.Value).OrderBy(p => p).ToList();
var localePlaceholders = Regex.Matches(localeValue, @"\{\d+\}")
.Select(m => m.Value).OrderBy(p => p).ToList();
Assert.True(enPlaceholders.SequenceEqual(localePlaceholders),
$"Locale '{locale}', key '{key}': expected placeholders " +
$"[{string.Join(", ", enPlaceholders)}] but found " +
$"[{string.Join(", ", localePlaceholders)}]");
}
}
}
[Fact]
public void AllFiveLocaleDirectories_Exist()
{
var stringsDir = GetStringsDirectory();
string[] expected = ["en-us", "fr-fr", "nl-nl", "zh-cn", "zh-tw"];
foreach (var locale in expected)
{
var dir = Path.Combine(stringsDir, locale);
Assert.True(Directory.Exists(dir), $"Locale directory missing: {locale}");
Assert.True(File.Exists(Path.Combine(dir, "Resources.resw")),
$"Resources.resw missing for locale: {locale}");
}
}
[Fact]
public void AllLocales_ContainOnboardingKeys()
{
var stringsDir = GetStringsDirectory();
var localeDirs = Directory.GetDirectories(stringsDir);
foreach (var localeDir in localeDirs)
{
var locale = Path.GetFileName(localeDir);
var reswPath = Path.Combine(localeDir, "Resources.resw");
if (!File.Exists(reswPath)) continue;
var keys = LoadResw(reswPath).Keys;
var onboardingKeys = keys.Where(k => k.StartsWith("Onboarding_")).ToList();
Assert.True(onboardingKeys.Count > 0,
$"Locale '{locale}' has no Onboarding_* keys");
}
}
[Fact]
public void AllLocales_ContainRuntimeOnboardingKeys()
{
var stringsDir = GetStringsDirectory();
var localeDirs = Directory.GetDirectories(stringsDir);
foreach (var localeDir in localeDirs)
{
var locale = Path.GetFileName(localeDir);
var reswPath = Path.Combine(localeDir, "Resources.resw");
if (!File.Exists(reswPath)) continue;
var keys = LoadResw(reswPath).Keys.ToHashSet(StringComparer.Ordinal);
var missing = RequiredRuntimeOnboardingKeys
.Where(key => !keys.Contains(key))
.ToList();
Assert.True(missing.Count == 0,
$"Locale '{locale}' is missing runtime onboarding key(s): {string.Join(", ", missing)}");
}
}
[Fact]
public void NoLocale_HasDuplicateKeys()
{
var stringsDir = GetStringsDirectory();
var localeDirs = Directory.GetDirectories(stringsDir);
foreach (var localeDir in localeDirs)
{
var locale = Path.GetFileName(localeDir);
var reswPath = Path.Combine(localeDir, "Resources.resw");
if (!File.Exists(reswPath)) continue;
var doc = System.Xml.Linq.XDocument.Load(reswPath);
var names = doc.Descendants("data")
.Select(e => e.Attribute("name")!.Value)
.ToList();
var duplicates = names.GroupBy(n => n)
.Where(g => g.Count() > 1)
.Select(g => g.Key)
.ToList();
Assert.True(duplicates.Count == 0,
$"Locale '{locale}' has duplicate keys: {string.Join(", ", duplicates)}");
}
}
[Fact]
public void AllLocales_HaveSameKeyCount()
{
var stringsDir = GetStringsDirectory();
var referencePath = Path.Combine(stringsDir, "en-us", "Resources.resw");
var referenceCount = LoadResw(referencePath).Count;
var localeDirs = Directory.GetDirectories(stringsDir);
foreach (var localeDir in localeDirs)
{
var locale = Path.GetFileName(localeDir);
var reswPath = Path.Combine(localeDir, "Resources.resw");
if (!File.Exists(reswPath)) continue;
var count = LoadResw(reswPath).Count;
Assert.Equal(referenceCount, count);
}
}
[Fact]
public void Resources_AreTranslatedAllOrNoneAcrossNonEnglishLocales()
{
var stringsDir = GetStringsDirectory();
var referenceResw = LoadResw(Path.Combine(stringsDir, "en-us", "Resources.resw"));
var localeResw = GetNonEnglishLocaleDirectories(stringsDir)
.Select(d => (Locale: Path.GetFileName(d), Resources: LoadResw(Path.Combine(d, "Resources.resw"))))
.ToList();
Assert.NotEmpty(localeResw);
var partial = new List();
var identicalWithoutRationale = new List();
foreach (var (key, enValue) in referenceResw.OrderBy(kv => kv.Key, StringComparer.Ordinal))
{
var identicalLocales = localeResw
.Where(l => l.Resources.TryGetValue(key, out var value) && value == enValue)
.Select(l => l.Locale)
.ToList();
if (identicalLocales.Count == 0)
continue;
if (identicalLocales.Count != localeResw.Count)
{
partial.Add($"{key} ({enValue}) identical in [{string.Join(", ", identicalLocales)}]");
continue;
}
if (!IsInvariantOrDeferred(key, enValue))
identicalWithoutRationale.Add($"{key} ({enValue})");
}
Assert.True(partial.Count == 0,
"Resources must be translated in all non-English locales or invariant in all. Partial entries: " +
string.Join("; ", partial.Take(20)));
Assert.True(identicalWithoutRationale.Count == 0,
"Resources identical to en-us in every non-English locale need an invariant/deferred rationale. Entries: " +
string.Join("; ", identicalWithoutRationale.Take(20)));
}
}