Skip to content

Commit 67a3598

Browse files
committed
feat: Add test to validate translation strings automatically
1 parent bc92cf3 commit 67a3598

File tree

7 files changed

+138
-2
lines changed

7 files changed

+138
-2
lines changed

.github/workflows/dotnet-build.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ jobs:
3131
uses: arduino/setup-task@v2
3232
with:
3333
repo-token: ${{ secrets.GITHUB_TOKEN }}
34+
35+
- name: Run Tests
36+
run: task test
3437

3538
- name: Build SyncTrayzor (Portable)
3639
run: task build-portable

Taskfile.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ tasks:
2020
- powershell -ExecutionPolicy Bypass -File scripts/upgrade-syncthing.ps1
2121
platforms: [windows]
2222
run: once
23+
test:
24+
desc: "Test SyncTrayzor"
25+
cmds:
26+
- powershell -ExecutionPolicy Bypass -File scripts/test.ps1
27+
platforms: [windows]
2328
build:
2429
desc: "Build SyncTrayzor"
2530
deps:

scripts/test.ps1

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
$ErrorActionPreference = "Stop"
2+
3+
dotnet test src/
4+
if ($LASTEXITCODE -ne 0) {
5+
Write-Error "Tests failed. Exiting."
6+
exit $LASTEXITCODE
7+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Globalization;
4+
using System.IO;
5+
using System.Linq;
6+
using System.Reflection;
7+
using System.Xml.Linq;
8+
using SyncTrayzor.Localization;
9+
using Xunit;
10+
11+
namespace SyncTrayzor.Tests
12+
{
13+
/// <summary>
14+
/// DummyArgument is a class that can be implicitly converted to various types required by SmartFormat strings.
15+
/// </summary>
16+
public class DummyArgument : IEnumerable<object>
17+
{
18+
private readonly List<object> _items = new() { "dummy" };
19+
20+
public override string ToString() => "dummy";
21+
22+
public static implicit operator int(DummyArgument _) => 1;
23+
public static implicit operator string(DummyArgument d) => d.ToString();
24+
25+
public IEnumerator<object> GetEnumerator() => _items.GetEnumerator();
26+
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() => GetEnumerator();
27+
}
28+
29+
public class LocalizerTests
30+
{
31+
private static readonly object[] DummyArgs =
32+
Enumerable.Repeat<object>(new DummyArgument(), 10).ToArray();
33+
34+
[Fact]
35+
public void FormatWithCulture_AllResourceStringsAreValidSmartFormatStrings()
36+
{
37+
var testAssembly = Assembly.GetExecutingAssembly();
38+
var testDir = Path.GetDirectoryName(testAssembly.Location);
39+
var resourcesDir = Path.Combine(testDir, "Resources");
40+
var resxFiles = Directory.GetFiles(resourcesDir, "Resources*.resx");
41+
42+
foreach (var resxFile in resxFiles)
43+
{
44+
var fileName = Path.GetFileNameWithoutExtension(resxFile);
45+
var culture = CultureInfo.GetCultureInfo("en-US");
46+
if (fileName.Contains('.'))
47+
{
48+
var cultureName = fileName[(fileName.LastIndexOf('.') + 1)..];
49+
culture = CultureInfo.GetCultureInfo(cultureName);
50+
}
51+
52+
var doc = XDocument.Load(resxFile);
53+
var dataElements = doc.Descendants("data");
54+
55+
foreach (var data in dataElements)
56+
{
57+
var name = data.Attribute("name")?.Value;
58+
var value = data.Element("value")?.Value;
59+
60+
if (string.IsNullOrEmpty(value))
61+
continue;
62+
63+
try
64+
{
65+
Localizer.FormatWithCulture(culture, value, DummyArgs);
66+
}
67+
catch (Exception ex)
68+
{
69+
throw new Exception(
70+
$"Invalid SmartFormat string in {Path.GetFileName(resxFile)}, key '{name}': {ex.Message}",
71+
ex);
72+
}
73+
}
74+
}
75+
}
76+
}
77+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0-windows10.0.17763.0</TargetFramework>
5+
<IsPackable>false</IsPackable>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
10+
<PackageReference Include="xunit" Version="2.9.3" />
11+
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
12+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
13+
<PrivateAssets>all</PrivateAssets>
14+
</PackageReference>
15+
</ItemGroup>
16+
17+
<ItemGroup>
18+
<None Include="..\SyncTrayzor\Properties\Resources*.resx" Link="Resources\%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
19+
</ItemGroup>
20+
21+
<ItemGroup>
22+
<ProjectReference Include="..\SyncTrayzor\SyncTrayzor.csproj" />
23+
</ItemGroup>
24+
25+
</Project>

src/SyncTrayzor.sln

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
1+
22
Microsoft Visual Studio Solution File, Format Version 12.00
33
# Visual Studio 14
44
VisualStudioVersion = 14.0.24720.0
@@ -15,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ChecksumUtil", "ChecksumUti
1515
EndProject
1616
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PortableInstaller", "PortableInstaller\PortableInstaller.csproj", "{1803AB89-4148-4DFC-AE7B-B4191BD6281C}"
1717
EndProject
18+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SyncTrayzor.Tests", "SyncTrayzor.Tests\SyncTrayzor.Tests.csproj", "{D52BD38A-8BA2-471A-9D67-EDA909A688E9}"
19+
EndProject
1820
Global
1921
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2022
Debug|Any CPU = Debug|Any CPU
@@ -75,6 +77,18 @@ Global
7577
{D1F89B3D-7967-4DC6-AE45-50A7817FE54F}.Release|x86.Build.0 = Release|Any CPU
7678
{D1F89B3D-7967-4DC6-AE45-50A7817FE54F}.Release|Any CPU.Build.0 = Release|Any CPU
7779
{D1F89B3D-7967-4DC6-AE45-50A7817FE54F}.Release|x64.Deploy.0 = Release|Any CPU
80+
{D52BD38A-8BA2-471A-9D67-EDA909A688E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
81+
{D52BD38A-8BA2-471A-9D67-EDA909A688E9}.Debug|Any CPU.Build.0 = Debug|Any CPU
82+
{D52BD38A-8BA2-471A-9D67-EDA909A688E9}.Debug|x64.ActiveCfg = Debug|Any CPU
83+
{D52BD38A-8BA2-471A-9D67-EDA909A688E9}.Debug|x64.Build.0 = Debug|Any CPU
84+
{D52BD38A-8BA2-471A-9D67-EDA909A688E9}.Debug|x86.ActiveCfg = Debug|Any CPU
85+
{D52BD38A-8BA2-471A-9D67-EDA909A688E9}.Debug|x86.Build.0 = Debug|Any CPU
86+
{D52BD38A-8BA2-471A-9D67-EDA909A688E9}.Release|Any CPU.ActiveCfg = Release|Any CPU
87+
{D52BD38A-8BA2-471A-9D67-EDA909A688E9}.Release|Any CPU.Build.0 = Release|Any CPU
88+
{D52BD38A-8BA2-471A-9D67-EDA909A688E9}.Release|x64.ActiveCfg = Release|Any CPU
89+
{D52BD38A-8BA2-471A-9D67-EDA909A688E9}.Release|x64.Build.0 = Release|Any CPU
90+
{D52BD38A-8BA2-471A-9D67-EDA909A688E9}.Release|x86.ActiveCfg = Release|Any CPU
91+
{D52BD38A-8BA2-471A-9D67-EDA909A688E9}.Release|x86.Build.0 = Release|Any CPU
7892
EndGlobalSection
7993
GlobalSection(SolutionProperties) = preSolution
8094
HideSolutionNode = FALSE

src/SyncTrayzor/Localization/Localizer.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,12 @@ public static string Translate(string key, params object[] parameters)
4343

4444
public static string F(string format, params object[] parameters)
4545
{
46-
return formatter.Format(Thread.CurrentThread.CurrentUICulture, format, parameters);
46+
return FormatWithCulture(Thread.CurrentThread.CurrentUICulture, format, parameters);
47+
}
48+
49+
public static string FormatWithCulture(CultureInfo culture, string format, params object[] parameters)
50+
{
51+
return formatter.Format(culture, format, parameters);
4752
}
4853

4954
public static string OriginalTranslation(string key)

0 commit comments

Comments
 (0)