Compare commits

...

3 Commits

Author SHA1 Message Date
Scott Hanselman
508610f56d fix: pin global.json to 10.0.100 with latestPatch
ARM64 runner only has 10.0.101, not 10.0.102. Using 10.0.100 as base
with latestPatch finds any 10.0.1xx SDK (101 on ARM64, 104 on x64).
Stays within the feature band where MSBuild 17.x is compatible.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-31 15:42:59 -07:00
Scott Hanselman
aa4ea5794b fix: use latestPatch in global.json to avoid MSBuild version conflict
latestFeature picks up SDK 10.0.201 on CI runners, which requires
MSBuild 18.0. The MSIX build step uses VS MSBuild 17.14.x, causing
build failures. latestPatch keeps us in 10.0.1xx where MSBuild 17.x
is compatible.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-31 14:14:36 -07:00
Scott Hanselman
c6b0289a3e chore: quick-win triage — global.json, test tooling sync, localization tests
- Add global.json to pin .NET SDK 10.0.x (rollForward: latestFeature) (closes #123)
- Sync xunit.runner.visualstudio 3.1.0→3.1.4, add coverlet.collector to Tray.Tests (closes #90)
- Add LocalizationValidationTests: key parity + format placeholder validation (closes #70)

All 598 tests pass (503 shared + 95 tray).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-03-31 14:02:28 -07:00
3 changed files with 125 additions and 1 deletions

6
global.json Normal file
View File

@ -0,0 +1,6 @@
{
"sdk": {
"version": "10.0.100",
"rollForward": "latestPatch"
}
}

View File

@ -0,0 +1,117 @@
using System.Text.RegularExpressions;
using System.Xml.Linq;
namespace OpenClaw.Tray.Tests;
/// <summary>
/// Validates that all localization resource files are consistent with en-us.
/// Catches missing/extra keys and broken format placeholders early — before translation PRs land.
/// </summary>
public class LocalizationValidationTests
{
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<string, string> 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);
}
[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 = Directory.GetDirectories(stringsDir)
.Where(d => !string.Equals(Path.GetFileName(d), "en-us", StringComparison.OrdinalIgnoreCase))
.ToList();
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 = Directory.GetDirectories(stringsDir)
.Where(d => !string.Equals(Path.GetFileName(d), "en-us", StringComparison.OrdinalIgnoreCase));
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)}]");
}
}
}
}

View File

@ -8,9 +8,10 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
</ItemGroup>
<ItemGroup>