Skip to content

Commit 4174619

Browse files
test: validate localization key parity and format placeholders
Add LocalizationValidationTests to catch regressions in translation PRs: - AllLocales_HaveExactlySameKeysAsEnUs: asserts every locale under Strings/ has exactly the same resource key set as en-us — no missing or extra entries. Runs against all present locales (zh-cn, and any future ones like fr-fr from PR #69). - AllLocales_PreserveFormatPlaceholders: asserts that {0}/{1}/... format placeholders in translated values match the en-us source, preventing runtime FormatException from mistranslated strings. 95 Tray tests pass (93 existing + 2 new). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 536d436 commit 4174619

File tree

1 file changed

+117
-0
lines changed

1 file changed

+117
-0
lines changed
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
using System.Text.RegularExpressions;
2+
using System.Xml.Linq;
3+
4+
namespace OpenClaw.Tray.Tests;
5+
6+
/// <summary>
7+
/// Validates that all localization resource files are consistent with en-us.
8+
/// Catches missing/extra keys and broken format placeholders early — before translation PRs land.
9+
/// </summary>
10+
public class LocalizationValidationTests
11+
{
12+
private static string GetRepositoryRoot()
13+
{
14+
var envRepoRoot = Environment.GetEnvironmentVariable("OPENCLAW_REPO_ROOT");
15+
if (!string.IsNullOrWhiteSpace(envRepoRoot) && Directory.Exists(envRepoRoot))
16+
return envRepoRoot;
17+
18+
var directory = new DirectoryInfo(AppContext.BaseDirectory);
19+
while (directory != null)
20+
{
21+
if (Directory.Exists(Path.Combine(directory.FullName, ".git")) &&
22+
File.Exists(Path.Combine(directory.FullName, "README.md")))
23+
return directory.FullName;
24+
directory = directory.Parent;
25+
}
26+
27+
throw new InvalidOperationException(
28+
"Could not find repository root. Set OPENCLAW_REPO_ROOT to the repo path.");
29+
}
30+
31+
private static string GetStringsDirectory() =>
32+
Path.Combine(GetRepositoryRoot(), "src", "OpenClaw.Tray.WinUI", "Strings");
33+
34+
private static Dictionary<string, string> LoadResw(string path)
35+
{
36+
var doc = XDocument.Load(path);
37+
return doc.Descendants("data")
38+
.ToDictionary(
39+
e => e.Attribute("name")!.Value,
40+
e => e.Element("value")?.Value ?? string.Empty);
41+
}
42+
43+
[Fact]
44+
public void AllLocales_HaveExactlySameKeysAsEnUs()
45+
{
46+
var stringsDir = GetStringsDirectory();
47+
var referencePath = Path.Combine(stringsDir, "en-us", "Resources.resw");
48+
Assert.True(File.Exists(referencePath), $"Reference file not found: {referencePath}");
49+
50+
var referenceKeys = LoadResw(referencePath).Keys.ToHashSet(StringComparer.Ordinal);
51+
52+
var localeDirs = Directory.GetDirectories(stringsDir)
53+
.Where(d => !string.Equals(Path.GetFileName(d), "en-us", StringComparison.OrdinalIgnoreCase))
54+
.ToList();
55+
56+
Assert.NotEmpty(localeDirs);
57+
58+
foreach (var localeDir in localeDirs)
59+
{
60+
var locale = Path.GetFileName(localeDir);
61+
var reswPath = Path.Combine(localeDir, "Resources.resw");
62+
Assert.True(File.Exists(reswPath), $"Expected Resources.resw for locale '{locale}'.");
63+
64+
var localeKeys = LoadResw(reswPath).Keys.ToHashSet(StringComparer.Ordinal);
65+
66+
var missing = referenceKeys.Except(localeKeys).OrderBy(k => k).ToList();
67+
var extra = localeKeys.Except(referenceKeys).OrderBy(k => k).ToList();
68+
69+
Assert.True(missing.Count == 0,
70+
$"Locale '{locale}' is missing {missing.Count} key(s): {string.Join(", ", missing.Take(10))}");
71+
Assert.True(extra.Count == 0,
72+
$"Locale '{locale}' has {extra.Count} unexpected key(s): {string.Join(", ", extra.Take(10))}");
73+
}
74+
}
75+
76+
[Fact]
77+
public void AllLocales_PreserveFormatPlaceholders()
78+
{
79+
var stringsDir = GetStringsDirectory();
80+
var referenceResw = LoadResw(Path.Combine(stringsDir, "en-us", "Resources.resw"));
81+
82+
var keysWithPlaceholders = referenceResw
83+
.Where(kv => Regex.IsMatch(kv.Value, @"\{\d+\}"))
84+
.ToList();
85+
86+
if (keysWithPlaceholders.Count == 0)
87+
return;
88+
89+
var localeDirs = Directory.GetDirectories(stringsDir)
90+
.Where(d => !string.Equals(Path.GetFileName(d), "en-us", StringComparison.OrdinalIgnoreCase));
91+
92+
foreach (var localeDir in localeDirs)
93+
{
94+
var locale = Path.GetFileName(localeDir);
95+
var reswPath = Path.Combine(localeDir, "Resources.resw");
96+
if (!File.Exists(reswPath)) continue;
97+
98+
var localeResw = LoadResw(reswPath);
99+
100+
foreach (var (key, enValue) in keysWithPlaceholders)
101+
{
102+
if (!localeResw.TryGetValue(key, out var localeValue))
103+
continue;
104+
105+
var enPlaceholders = Regex.Matches(enValue, @"\{\d+\}")
106+
.Select(m => m.Value).OrderBy(p => p).ToList();
107+
var localePlaceholders = Regex.Matches(localeValue, @"\{\d+\}")
108+
.Select(m => m.Value).OrderBy(p => p).ToList();
109+
110+
Assert.True(enPlaceholders.SequenceEqual(localePlaceholders),
111+
$"Locale '{locale}', key '{key}': expected placeholders " +
112+
$"[{string.Join(", ", enPlaceholders)}] but found " +
113+
$"[{string.Join(", ", localePlaceholders)}]");
114+
}
115+
}
116+
}
117+
}

0 commit comments

Comments
 (0)