Skip to content

Commit

Permalink
Add Localization tests
Browse files Browse the repository at this point in the history
  • Loading branch information
tj-devel709 committed Oct 30, 2024
1 parent 56c2a3d commit 4fbd601
Show file tree
Hide file tree
Showing 2 changed files with 215 additions and 0 deletions.
31 changes: 31 additions & 0 deletions src/Core/tests/UnitTests/Core.UnitTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,37 @@
<ProjectReference Include="..\..\..\Controls\src\Core\Controls.Core.csproj" />
<ProjectReference Include="..\..\..\TestUtils\src\TestUtils\TestUtils.csproj" />
<ProjectReference Include="..\..\src\Core.csproj" />

<EmbeddedResource Include="..\..\..\Controls\src\Core\Compatibility\iOS\Resources\StringResources.resx" Link="Resources\StringResources.resx" />
<EmbeddedResource Include="..\..\..\Controls\src\Core\Compatibility\iOS\Resources\StringResources.cs.resx" Link="Resources\StringResources.cs.resx" />
<EmbeddedResource Include="..\..\..\Controls\src\Core\Compatibility\iOS\Resources\StringResources.de.resx" Link="Resources\StringResources.de.resx" />
<EmbeddedResource Include="..\..\..\Controls\src\Core\Compatibility\iOS\Resources\StringResources.es.resx" Link="Resources\StringResources.es.resx" />
<EmbeddedResource Include="..\..\..\Controls\src\Core\Compatibility\iOS\Resources\StringResources.fr.resx" Link="Resources\StringResources.fr.resx" />
<EmbeddedResource Include="..\..\..\Controls\src\Core\Compatibility\iOS\Resources\StringResources.it.resx" Link="Resources\StringResources.it.resx" />
<EmbeddedResource Include="..\..\..\Controls\src\Core\Compatibility\iOS\Resources\StringResources.ja.resx" Link="Resources\StringResources.ja.resx" />
<EmbeddedResource Include="..\..\..\Controls\src\Core\Compatibility\iOS\Resources\StringResources.ko.resx" Link="Resources\StringResources.ko.resx" />
<EmbeddedResource Include="..\..\..\Controls\src\Core\Compatibility\iOS\Resources\StringResources.pl.resx" Link="Resources\StringResources.pl.resx" />
<EmbeddedResource Include="..\..\..\Controls\src\Core\Compatibility\iOS\Resources\StringResources.pt-BR.resx" Link="Resources\StringResources.pt-BR.resx" />
<EmbeddedResource Include="..\..\..\Controls\src\Core\Compatibility\iOS\Resources\StringResources.ru.resx" Link="Resources\StringResources.ru.resx" />
<EmbeddedResource Include="..\..\..\Controls\src\Core\Compatibility\iOS\Resources\StringResources.tr.resx" Link="Resources\StringResources.tr.resx" />
<EmbeddedResource Include="..\..\..\Controls\src\Core\Compatibility\iOS\Resources\StringResources.zh-Hans.resx" Link="Resources\StringResources.zh-Hans.resx" />
<EmbeddedResource Include="..\..\..\Controls\src\Core\Compatibility\iOS\Resources\StringResources.zh-Hant.resx" Link="Resources\StringResources.zh-Hant.resx" />

<EmbeddedResource Include="..\..\..\Controls\src\Build.Tasks\ErrorMessages.resx" Link="Resources\ErrorMessages.resx" />
<EmbeddedResource Include="..\..\..\Controls\src\Build.Tasks\xlf\ErrorMessages.cs.resx" Link="Resources\ErrorMessages.cs.resx" />
<EmbeddedResource Include="..\..\..\Controls\src\Build.Tasks\xlf\ErrorMessages.de.resx" Link="Resources\ErrorMessages.de.resx" />
<EmbeddedResource Include="..\..\..\Controls\src\Build.Tasks\xlf\ErrorMessages.es.resx" Link="Resources\ErrorMessages.es.resx" />
<EmbeddedResource Include="..\..\..\Controls\src\Build.Tasks\xlf\ErrorMessages.fr.resx" Link="Resources\ErrorMessages.fr.resx" />
<EmbeddedResource Include="..\..\..\Controls\src\Build.Tasks\xlf\ErrorMessages.it.resx" Link="Resources\ErrorMessages.it.resx" />
<EmbeddedResource Include="..\..\..\Controls\src\Build.Tasks\xlf\ErrorMessages.ja.resx" Link="Resources\ErrorMessages.ja.resx" />
<EmbeddedResource Include="..\..\..\Controls\src\Build.Tasks\xlf\ErrorMessages.ko.resx" Link="Resources\ErrorMessages.ko.resx" />
<EmbeddedResource Include="..\..\..\Controls\src\Build.Tasks\xlf\ErrorMessages.pl.resx" Link="Resources\ErrorMessages.pl.resx" />
<EmbeddedResource Include="..\..\..\Controls\src\Build.Tasks\xlf\ErrorMessages.pt-BR.resx" Link="Resources\ErrorMessages.pt-BR.resx" />
<EmbeddedResource Include="..\..\..\Controls\src\Build.Tasks\xlf\ErrorMessages.ru.resx" Link="Resources\ErrorMessages.ru.resx" />
<EmbeddedResource Include="..\..\..\Controls\src\Build.Tasks\xlf\ErrorMessages.tr.resx" Link="Resources\ErrorMessages.tr.resx" />
<EmbeddedResource Include="..\..\..\Controls\src\Build.Tasks\xlf\ErrorMessages.zh-Hans.resx" Link="Resources\ErrorMessages.zh-Hans.resx" />
<EmbeddedResource Include="..\..\..\Controls\src\Build.Tasks\xlf\ErrorMessages.zh-Hant.resx" Link="Resources\ErrorMessages.zh-Hant.resx" />

</ItemGroup>

</Project>
184 changes: 184 additions & 0 deletions src/Core/tests/UnitTests/LocalizationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using System.Globalization;
using System.Resources;
using Xunit;


namespace Microsoft.Maui.UnitTests
{
[Category(TestCategory.Core)]
public class LocalizationTests : IClassFixture<CultureFixture>
{
string _mauiRoot = Path.Combine("..", "..", "..", "..", "..");

/// <summary>
/// This test checks that all the .lcl files' <str> elements have a corresponding
/// <tgt> element with their translations. Since there may be times that we have untranslated
/// elements, instead of failing the test when something is missing, the strings without
/// translations will be stored in a txt file for viewing: artifacts/bin/Core.UnitTests/Debug/net9.0/localizationTestsOutput/LocalizationMissingTargets.txt
/// </summary>
[Fact]
public void LocalizationStringsAreTranslated()
{
string lclFilePath = Path.Combine(_mauiRoot, "loc");
string outputFilePath = Path.Combine("localizationTestsOutput", "LocalizationMissingTargets.txt");

// Ensure the directory exists
var directoryPath = Path.GetDirectoryName(outputFilePath);
if (!Directory.Exists(directoryPath))
{
Directory.CreateDirectory(directoryPath);
}

var lclFiles = Directory.GetFiles(lclFilePath, "*.lcl", SearchOption.AllDirectories);

using (var writer = new StreamWriter(outputFilePath))
{
foreach (var file in lclFiles)
{
var localizedKeys = LoadKeysFromLcl(file);

foreach (var key in localizedKeys)
{
if (key.Value.Target is null)
{
var filePath = file.Replace(_mauiRoot, "", StringComparison.Ordinal);
writer.WriteLine($"File: {filePath}, Key: {key.Key}, Source: {key.Value.Source}");
}
}
}
}
}

/// <summary>
/// This test checks that the strings inside the resx files are actually translated inside the assemblies
/// when the locale is changed. It compares the actual value inside the assembly to the culture specific .lcl file.
/// Strings that are not translated will be stored in a txt file for viewing:
/// artifacts/bin/Core.UnitTests/Debug/net9.0/localizationTestsOutput/LocalizationTranslationsNotDisplayed_<locale>.txt
/// </summary>
[Theory]
[InlineData("cs", "cs-CZ")]
[InlineData("de", "de-DE")]
[InlineData("es", "es-ES")]
[InlineData("fr", "fr-FR")]
[InlineData("it", "it-IT")]
[InlineData("ja", "ja-JP")]
[InlineData("ko", "ko-KR")]
[InlineData("pl", "pl-PL")]
[InlineData("pt-BR", "pt-BR")]
[InlineData("ru", "ru-RU")]
[InlineData("tr", "tr-TR")]
[InlineData("zh-Hans", "zh-Hans")]
[InlineData("zh-Hant", "zh-Hant")]
public void LocalizationStringsAreDisplayedProperly(string cultureAbbreviation, string culture)
{
string lclFilePath = Path.Combine(_mauiRoot, "loc", cultureAbbreviation);

var lclFiles = Directory.GetFiles(lclFilePath, "*.resx.lcl", SearchOption.AllDirectories);

CultureInfo.CurrentCulture = new CultureInfo(culture);
CultureInfo.CurrentUICulture = new CultureInfo(culture);

var count = 0;

var outputFilePath = Path.Combine("localizationTestsOutput", "LocalizationTranslationsNotDisplayed_" + cultureAbbreviation + ".txt");

// Ensure the directory exists
var directoryPath = Path.GetDirectoryName(outputFilePath);
if (!Directory.Exists(directoryPath))
{
Directory.CreateDirectory(directoryPath);
}

using (var writer = new StreamWriter(outputFilePath))
{
foreach (var file in lclFiles)
{
string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(file);
string resourceName = fileNameWithoutExtension.Split('.')[0];
var filePath = file.Replace(_mauiRoot, "", StringComparison.Ordinal);
var localizedKeys = LoadKeysFromLcl(file);

// Check each key in the localization
foreach (var key in localizedKeys)
{
if (key.Value.Target is not null)
{
// Get the actual translation from the assembly
var keyName = key.Key[1..];
string actualTranslation = GetLocalizedString(resourceName, keyName);

// Compare with the expected translation in the .lcl file
if (key.Value.Target != actualTranslation)
{
writer.WriteLine($"File: {filePath}, Key: {keyName}, Expected: {key.Value.Target}, Actual: {actualTranslation}");
}
else
{
count++;
}
}
}
}

writer.WriteLine($"Number of correctly displayed translated strings: {count}");
}
}

public Dictionary<string, (string Source, string Target)> LoadKeysFromLcl(string filePath)
{
var translations = new Dictionary<string, (string Source, string Target)>();
var xmlDoc = new XmlDocument();
xmlDoc.Load(filePath);

var namespaceManager = new XmlNamespaceManager(xmlDoc.NameTable);
namespaceManager.AddNamespace("lcx", "http://schemas.microsoft.com/locstudio/2006/6/lcx");

var itemNodes = xmlDoc.SelectNodes("//lcx:Item", namespaceManager);

foreach (XmlNode itemNode in itemNodes)
{
var itemId = itemNode.Attributes["ItemId"]?.Value;

var sourceValue = itemNode.SelectSingleNode("lcx:Str/lcx:Val", namespaceManager)?.InnerText.Trim();
var targetValue = itemNode.SelectSingleNode("lcx:Str/lcx:Tgt/lcx:Val", namespaceManager)?.InnerText.Trim();

if (!string.IsNullOrEmpty(itemId) && sourceValue is not null)
{
translations[itemId] = (sourceValue, targetValue);
}
}

return translations;
}

private string GetLocalizedString(string resourceString, string key)
{
var resourceManager = new ResourceManager("Core.UnitTests.Resources." + resourceString, typeof(LocalizationTests).Assembly);
return resourceManager.GetString(key, CultureInfo.CurrentUICulture);
}
}

public class CultureFixture : IDisposable
{
CultureInfo _originalCulture;
CultureInfo _originalUICulture;

public CultureFixture()
{
// Save the current culture and UI culture
_originalCulture = CultureInfo.CurrentCulture;
_originalUICulture = CultureInfo.CurrentUICulture;
}

public void Dispose()
{
// Restore the original culture and UI culture
CultureInfo.CurrentCulture = _originalCulture;
CultureInfo.CurrentUICulture = _originalUICulture;
}
}
}

0 comments on commit 4fbd601

Please sign in to comment.