314 lines
16 KiB
C#
314 lines
16 KiB
C#
using System.ComponentModel;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using System.Reflection;
|
|
using System.Text.Json;
|
|
using System.Text.RegularExpressions;
|
|
using System.Windows.Media.Media3D;
|
|
using CADability.GeoObject;
|
|
using STPViewer.Models;
|
|
using STPViewer.Services;
|
|
using STPViewer.ViewModels;
|
|
using IOPath = System.IO.Path;
|
|
|
|
namespace SmokeTest;
|
|
|
|
internal static class LocalizationTest
|
|
{
|
|
private static readonly Regex FormatIndex = new(@"(?<!\{)\{(\d+)(?:[^{}]*)\}(?!\})", RegexOptions.Compiled);
|
|
|
|
public static int Run()
|
|
{
|
|
int failures = 0;
|
|
Exception? error = null;
|
|
var thread = new Thread(() =>
|
|
{
|
|
try { failures = RunCore(); }
|
|
catch (Exception ex) { error = ex; }
|
|
});
|
|
thread.SetApartmentState(ApartmentState.STA);
|
|
thread.Start();
|
|
thread.Join();
|
|
if (error is not null)
|
|
{
|
|
Console.WriteLine($"LocalizationTest: {error}");
|
|
return 1;
|
|
}
|
|
return failures;
|
|
}
|
|
|
|
private static int RunCore()
|
|
{
|
|
int failures = CheckTemporaryPacks() + CheckBundledPacks() + CheckViewModel() + CheckSettings();
|
|
Console.WriteLine(failures == 0 ? "LocalizationTest: all passed" : $"LocalizationTest: {failures} failed");
|
|
return failures;
|
|
}
|
|
|
|
private static int CheckTemporaryPacks()
|
|
{
|
|
int failures = 0;
|
|
string directory = IOPath.Combine(IOPath.GetTempPath(), "stpviewer_i18n_" + Guid.NewGuid().ToString("N"));
|
|
try
|
|
{
|
|
failures += Check("missing directory loads without languages", new LocalizationService(directory).Languages.Count == 0);
|
|
Directory.CreateDirectory(directory);
|
|
failures += Check("empty directory loads without languages", new LocalizationService(directory).Languages.Count == 0);
|
|
WritePack(directory, "en", "English", new()
|
|
{
|
|
["Ui.Sample"] = "View", ["Ui.Fallback"] = "Fallback {0}",
|
|
["Ui.BadBrace"] = "English {0}", ["Ui.BadIndex"] = "English {0}",
|
|
["Ui.BothBad"] = "English {"
|
|
});
|
|
WritePack(directory, "zh_cn", "Simplified", new()
|
|
{
|
|
["Ui.Sample"] = "CN_VIEW", ["Ui.BadBrace"] = "Chinese {",
|
|
["Ui.BadIndex"] = "Chinese {1}", ["Ui.BothBad"] = "Chinese {"
|
|
});
|
|
WritePack(directory, "zh_tw", "Traditional", new() { ["Ui.Sample"] = "TW_VIEW" });
|
|
WritePack(directory, "pirate_dialect", "Pirate", new() { ["Ui.Sample"] = "ARR_VIEW" });
|
|
File.WriteAllText(IOPath.Combine(directory, "broken.json"), "{invalid json");
|
|
File.WriteAllText(IOPath.Combine(directory, "no_key.json"), "{\"data\":{\"Ui.Sample\":\"unused\"}}");
|
|
File.WriteAllText(IOPath.Combine(directory, "no_data.json"), "{\"key\":\"Missing data\"}");
|
|
File.WriteAllText(IOPath.Combine(directory, "non_string.json"), "{\"key\":\"Invalid\",\"data\":{\"Ui.Sample\":42}}");
|
|
|
|
CultureInfo oldUi = CultureInfo.CurrentUICulture;
|
|
LocalizationService service;
|
|
try
|
|
{
|
|
CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("en-US");
|
|
service = new LocalizationService(directory);
|
|
}
|
|
finally { CultureInfo.CurrentUICulture = oldUi; }
|
|
|
|
failures += Check("valid packs isolated from malformed files", service.Languages.Count == 4 &&
|
|
service.Languages.Select(l => l.Id).Order().SequenceEqual(new[] { "en", "pirate_dialect", "zh_cn", "zh_tw" }));
|
|
failures += Check("display names and data loaded", service.Languages.Any(l =>
|
|
l.Id == "pirate_dialect" && l.Key == "Pirate" && l.Data["Ui.Sample"] == "ARR_VIEW"));
|
|
failures += Check("English selected from system culture", service.Language == "en");
|
|
failures += Check("legacy Chinese IDs resolved", service.ResolveLanguage("zh-CN") == "zh_cn" &&
|
|
service.ResolveLanguage("zh-TW") == "zh_tw");
|
|
failures += Check("Chinese script and region aliases resolved", service.ResolveLanguage("zh-Hans") == "zh_cn" &&
|
|
service.ResolveLanguage("zh-SG") == "zh_cn" && service.ResolveLanguage("zh-Hant") == "zh_tw" &&
|
|
service.ResolveLanguage("zh-HK") == "zh_tw" && service.ResolveLanguage("zh-MO") == "zh_tw");
|
|
failures += Check("custom language ID resolved", service.ResolveLanguage("pirate-dialect") == "pirate_dialect");
|
|
failures += Check("unknown language falls back", service.ResolveLanguage("unknown") == "en");
|
|
failures += Check("invalid custom ID falls back", service.ResolveLanguage("pirate_other") == "en");
|
|
|
|
CultureInfo beforeNull = CultureInfo.CurrentUICulture;
|
|
try
|
|
{
|
|
CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo("zh-HK");
|
|
failures += Check("null language uses system UI culture", service.ResolveLanguage(null) == "zh_tw");
|
|
}
|
|
finally { CultureInfo.CurrentUICulture = beforeNull; }
|
|
|
|
LanguageDefinition chinese = service.Languages.Single(l => l.Id == "zh_cn");
|
|
bool selectedNotified = false, indexerNotified = false;
|
|
chinese.PropertyChanged += (_, e) => selectedNotified |= e.PropertyName == nameof(LanguageDefinition.IsSelected);
|
|
((INotifyPropertyChanged)service).PropertyChanged += (_, e) => indexerNotified |= e.PropertyName == "Item[]";
|
|
service.SetLanguage("zh-CN");
|
|
failures += Check("selection and indexer notify", service.Language == "zh_cn" &&
|
|
chinese.IsSelected && selectedNotified && indexerNotified);
|
|
failures += Check("selected data and fallback", service["Ui.Sample"] == "CN_VIEW" &&
|
|
service["Ui.Fallback"] == "Fallback {0}" && service["Ui.Missing"] == "Ui.Missing");
|
|
failures += Check("malformed brace uses English format", service.FormatText("Ui.BadBrace", "value") == "English value");
|
|
failures += Check("missing format argument uses English format", service.FormatText("Ui.BadIndex", "value") == "English value");
|
|
failures += Check("invalid English format returns key", service.FormatText("Ui.BothBad", "value") == "Ui.BothBad");
|
|
service.SetLanguage("pirate_dialect");
|
|
failures += Check("custom language selected", service.Language == "pirate_dialect" &&
|
|
service["Ui.Sample"] == "ARR_VIEW");
|
|
|
|
WritePack(directory, "new_language", "New language", new() { ["Ui.Sample"] = "NEW_VIEW" });
|
|
WritePack(directory, "en", "English", new() { ["Ui.Sample"] = "Updated view", ["Ui.Fallback"] = "Fallback {0}" });
|
|
failures += Check("existing instance does not hot reload", service.Languages.Count == 4 &&
|
|
service.Languages.Single(l => l.Id == "en").Data["Ui.Sample"] == "View");
|
|
var reloaded = new LocalizationService(directory);
|
|
failures += Check("new instance rescans files and values", reloaded.Languages.Count == 5 &&
|
|
reloaded.Languages.Single(l => l.Id == "en").Data["Ui.Sample"] == "Updated view");
|
|
|
|
File.Delete(IOPath.Combine(directory, "zh_tw.json"));
|
|
var withoutTraditional = new LocalizationService(directory);
|
|
failures += Check("missing language not listed", withoutTraditional.Languages.All(l => l.Id != "zh_tw"));
|
|
failures += Check("missing legacy ID falls back", withoutTraditional.ResolveLanguage("zh-TW") == "en");
|
|
}
|
|
finally
|
|
{
|
|
if (Directory.Exists(directory)) Directory.Delete(directory, recursive: true);
|
|
}
|
|
return failures;
|
|
}
|
|
|
|
private static void WritePack(string directory, string id, string name, Dictionary<string, string> data) =>
|
|
File.WriteAllText(IOPath.Combine(directory, id + ".json"), JsonSerializer.Serialize(new { key = name, data }));
|
|
|
|
private static int CheckBundledPacks()
|
|
{
|
|
int failures = 0;
|
|
LocalizationService service = LocalizationService.Instance;
|
|
failures += Check("bundled languages available", new[] { "en", "zh_cn", "zh_tw" }
|
|
.All(id => service.Languages.Any(l => l.Id == id)));
|
|
LanguageDefinition? english = service.Languages.FirstOrDefault(l => l.Id == "en");
|
|
if (english is null) return failures;
|
|
failures += Check("bundled English data nonempty", english.Data.Count > 0 &&
|
|
english.Data.Values.All(v => !string.IsNullOrWhiteSpace(v)));
|
|
int formatFailures = 0;
|
|
foreach (LanguageDefinition language in service.Languages)
|
|
foreach ((string key, string value) in language.Data)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value)) formatFailures++;
|
|
int[] indices = Indices(value);
|
|
if (english.Data.TryGetValue(key, out string? fallback) && !Indices(fallback).SequenceEqual(indices))
|
|
{
|
|
Console.WriteLine($" FAIL: {language.Id} format arguments differ for {key}");
|
|
formatFailures++;
|
|
}
|
|
object[] args = Enumerable.Range(0, indices.DefaultIfEmpty(-1).Max() + 1)
|
|
.Select(i => (object)i).ToArray();
|
|
try { _ = string.Format(CultureInfo.InvariantCulture, value, args); }
|
|
catch (FormatException)
|
|
{
|
|
Console.WriteLine($" FAIL: {language.Id} format syntax for {key}");
|
|
formatFailures++;
|
|
}
|
|
}
|
|
failures += Check("bundled data and format strings valid", formatFailures == 0);
|
|
failures += Check("missing key returns key", service["__missing_localization_key__"] == "__missing_localization_key__");
|
|
return failures;
|
|
}
|
|
|
|
private static int CheckViewModel()
|
|
{
|
|
int failures = 0;
|
|
LocalizationService service = LocalizationService.Instance;
|
|
string originalLanguage = service.Language;
|
|
CultureInfo numericCulture = CultureInfo.CurrentCulture;
|
|
try
|
|
{
|
|
service.SetLanguage("en");
|
|
var vm = new MainViewModel();
|
|
vm.InitializeLocalization();
|
|
try
|
|
{
|
|
string oldLabel = vm.QuickBar["Import"].Label;
|
|
bool labelChanged = false;
|
|
vm.QuickBar["Import"].PropertyChanged += (_, e) =>
|
|
labelChanged |= e.PropertyName == nameof(QuickBarItemViewModel.Label);
|
|
|
|
MeasurementResult result = new MeasurementService().MeasureDistance(
|
|
new Point3D(0, 0, 0), new Point3D(25.4, 0, 0), "D1", 0.2);
|
|
vm.Measurements.Add(result);
|
|
vm.UseInch = true;
|
|
string oneInch = 1d.ToString("F4", numericCulture) + " in";
|
|
string beforeTitle = result.Title, beforeDetail = result.Detail;
|
|
string beforeLabel = result.DynamicLabels.Single().Label.Text;
|
|
var (distanceLabel, distanceTextFor) = result.DynamicLabels.Single();
|
|
int labelRefreshes = 0;
|
|
result.DynamicLabels[0] = (distanceLabel, u =>
|
|
{
|
|
labelRefreshes++;
|
|
return distanceTextFor(u);
|
|
});
|
|
bool titleChanged = false, detailChanged = false;
|
|
result.PropertyChanged += (_, e) =>
|
|
{
|
|
titleChanged |= e.PropertyName == nameof(MeasurementResult.Title);
|
|
detailChanged |= e.PropertyName == nameof(MeasurementResult.Detail);
|
|
};
|
|
|
|
vm.ChangeLanguageCommand.Execute("zh_tw");
|
|
failures += Check("VM selects language", service.Language == "zh_tw" &&
|
|
service.Languages.Single(l => l.Id == "zh_tw").IsSelected);
|
|
failures += Check("existing distance detail translated", beforeDetail != result.Detail &&
|
|
titleChanged && detailChanged && result.Detail.Contains(oneInch, StringComparison.Ordinal));
|
|
failures += Check("distance and number culture preserved", vm.UseInch &&
|
|
CultureInfo.CurrentCulture.Equals(numericCulture) && beforeTitle == result.Title &&
|
|
result.Title.Contains(oneInch, StringComparison.Ordinal));
|
|
failures += Check("3D distance label refreshed", labelRefreshes == 1 &&
|
|
beforeLabel == oneInch && result.DynamicLabels.Single().Label.Text == beforeLabel);
|
|
failures += Check("quick bar label refreshed", labelChanged && vm.QuickBar["Import"].Label != oldLabel);
|
|
|
|
LanguageDefinition selected = service.Languages.Single(l => l.Id == "zh_tw");
|
|
bool reselected = false;
|
|
selected.PropertyChanged += (_, e) => reselected |= e.PropertyName == nameof(LanguageDefinition.IsSelected);
|
|
vm.ChangeLanguageCommand.Execute("zh_tw");
|
|
failures += Check("reselecting current language restores check", reselected && selected.IsSelected);
|
|
}
|
|
finally { vm.DisposeLocalization(); }
|
|
failures += CheckImportedNames(service);
|
|
}
|
|
finally { service.SetLanguage(originalLanguage); }
|
|
return failures;
|
|
}
|
|
|
|
private static int CheckImportedNames(LocalizationService service)
|
|
{
|
|
int failures = 0;
|
|
MethodInfo blockNode = typeof(StepImportService).GetMethod("BlockNode", BindingFlags.NonPublic | BindingFlags.Instance)!;
|
|
var importer = new StepImportService();
|
|
service.SetLanguage("en");
|
|
|
|
ImportedNode Import(Block block) => (ImportedNode)blockNode.Invoke(importer, [block])!;
|
|
var namedSolidBlock = Block.Construct();
|
|
namedSolidBlock.Name = "Solid";
|
|
namedSolidBlock.Add(Solid.Construct());
|
|
ImportedNode namedSolid = Import(namedSolidBlock);
|
|
var namedShellBlock = Block.Construct();
|
|
namedShellBlock.Name = "Shell";
|
|
namedShellBlock.Add(Shell.Construct());
|
|
ImportedNode namedShell = Import(namedShellBlock);
|
|
var defaultSolidBlock = Block.Construct();
|
|
defaultSolidBlock.Add(Solid.Construct());
|
|
ImportedNode defaultSolid = Import(defaultSolidBlock);
|
|
var defaultShellBlock = Block.Construct();
|
|
defaultShellBlock.Add(Shell.Construct());
|
|
ImportedNode defaultShellLeaf = Import(defaultShellBlock).Children.Single();
|
|
var collisionBlock = Block.Construct();
|
|
var realComponentShell = Shell.Construct();
|
|
realComponentShell.Name = "Component";
|
|
collisionBlock.Add(realComponentShell);
|
|
ImportedNode collision = Import(collisionBlock);
|
|
|
|
ModelNodeViewModel ViewModel(ImportedNode node) => new(0)
|
|
{
|
|
Name = node.Name,
|
|
NameKey = node.NameKey,
|
|
NameNumber = node.NameNumber
|
|
};
|
|
var namedSolidVm = ViewModel(namedSolid);
|
|
var namedShellVm = ViewModel(namedShell);
|
|
var defaultSolidVm = ViewModel(defaultSolid);
|
|
var defaultShellVm = ViewModel(defaultShellLeaf);
|
|
var collisionVm = ViewModel(collision);
|
|
service.SetLanguage("zh_cn");
|
|
failures += Check("explicit solid name retained", namedSolid.NameKey is null && namedSolidVm.Name == "Solid");
|
|
failures += Check("real block name retained", namedShell.NameKey is null && namedShellVm.Name == "Shell");
|
|
failures += Check("default solid name translated", defaultSolid.NameKey == "Runtime.SolidName" &&
|
|
defaultSolidVm.Name == service["Runtime.SolidName"]);
|
|
failures += Check("default shell name translated", defaultShellLeaf.NameKey == "Runtime.ShellName" &&
|
|
defaultShellVm.Name == service["Runtime.ShellName"]);
|
|
failures += Check("real child name wins collision", collision.NameKey is null && collisionVm.Name == "Component");
|
|
return failures;
|
|
}
|
|
|
|
private static int CheckSettings()
|
|
{
|
|
var settings = new AppSettings
|
|
{
|
|
Language = "zh-CN", WindowWidth = 1200, WindowHeight = 800, WindowLeft = 100, WindowTop = 100
|
|
};
|
|
AppSettings? restored = JsonSerializer.Deserialize<AppSettings>(JsonSerializer.Serialize(settings));
|
|
return Check("legacy ID round trips through settings JSON", restored?.Language == "zh-CN" &&
|
|
LocalizationService.Instance.ResolveLanguage(restored.Language) == "zh_cn");
|
|
}
|
|
|
|
private static int[] Indices(string format) => FormatIndex.Matches(format)
|
|
.Select(m => int.Parse(m.Groups[1].Value, CultureInfo.InvariantCulture))
|
|
.Distinct().Order().ToArray();
|
|
|
|
private static int Check(string name, bool ok)
|
|
{
|
|
Console.WriteLine($" {(ok ? "PASS" : "FAIL")}: {name}");
|
|
return ok ? 0 : 1;
|
|
}
|
|
}
|