Browse Source

Refactor localization handling and improve test coverage for split localization files

pull/25227/head
maliming 5 months ago
parent
commit
f7c22b4123
No known key found for this signature in database GPG Key ID: A646B9CB645ECEA4
  1. 2
      docs/en/framework/fundamentals/localization.md
  2. 2
      framework/src/Volo.Abp.Localization/Volo/Abp/Localization/ILocalizationDictionary.cs
  3. 19
      framework/src/Volo.Abp.Localization/Volo/Abp/Localization/StaticLocalizationDictionary.cs
  4. 15
      framework/src/Volo.Abp.Localization/Volo/Abp/Localization/VirtualFiles/VirtualFileLocalizationResourceContributorBase.cs
  5. 22
      framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpLocalization_Tests.cs
  6. 40
      framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpStringLocalizerFactory_Tests.cs
  7. 47
      framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/JsonLocalizationDictionaryBuilder_Tests.cs

2
docs/en/framework/fundamentals/localization.md

@ -128,7 +128,7 @@ var str3 = L["Hi__1"]; // Hello World!
You can have more than one localization file with the same culture: files will be merged.
> Note: if you have the same key with the same culture then last value will win. This is true in case of file-per-culture and several files per culture. Files are ordered alphabetically, so if a same key defined in files `!en.json` and `z_en.json` then value from file `z_en.json` will be used.
> Note: If the same key is defined in multiple files for the same culture, the last value wins. Files are sorted by name using ordinal (byte-order) string comparison before merging, so if `!en.json` and `zen_Book.json` both define the same key, the value from `zen_Book.json` is used.
### Default Resource

2
framework/src/Volo.Abp.Localization/Volo/Abp/Localization/ILocalizationDictionary.cs

@ -13,6 +13,4 @@ public interface ILocalizationDictionary
LocalizedString? GetOrNull(string name);
void Fill(Dictionary<string, LocalizedString> dictionary);
void Merge(ILocalizationDictionary dictionary);
}

19
framework/src/Volo.Abp.Localization/Volo/Abp/Localization/StaticLocalizationDictionary.cs

@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Localization;
@ -38,22 +37,4 @@ public class StaticLocalizationDictionary : ILocalizationDictionary
dictionary[item.Key] = item.Value;
}
}
public void Merge(ILocalizationDictionary dictionary)
{
if (dictionary is not StaticLocalizationDictionary staticLocalizationDictionary)
{
return; // do nothing, we have no idea what to do with such
}
foreach (var item in staticLocalizationDictionary.Dictionary)
{
if (string.IsNullOrEmpty(item.Key))
{
throw new AbpException("The key is empty in given json string.");
}
Dictionary[item.Key] = item.Value;
}
}
}

15
framework/src/Volo.Abp.Localization/Volo/Abp/Localization/VirtualFiles/VirtualFileLocalizationResourceContributorBase.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.FileProviders;
@ -90,13 +91,10 @@ public abstract class VirtualFileLocalizationResourceContributorBase : ILocaliza
{
var dictionaries = new Dictionary<string, ILocalizationDictionary>();
foreach (var file in _virtualFileProvider.GetDirectoryContents(_virtualPath))
foreach (var file in _virtualFileProvider.GetDirectoryContents(_virtualPath)
.Where(f => !f.IsDirectory && CanParseFile(f))
.OrderBy(f => f.Name, StringComparer.Ordinal))
{
if (file.IsDirectory || !CanParseFile(file))
{
continue;
}
var dictionary = CreateDictionaryFromFile(file);
if (dictionary == null)
@ -110,7 +108,10 @@ public abstract class VirtualFileLocalizationResourceContributorBase : ILocaliza
}
else
{
dictionaries[dictionary.CultureName].Merge(dictionary);
var merged = new Dictionary<string, LocalizedString>();
dictionaries[dictionary.CultureName].Fill(merged);
dictionary.Fill(merged);
dictionaries[dictionary.CultureName] = new StaticLocalizationDictionary(dictionary.CultureName, merged);
}
}

22
framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpLocalization_Tests.cs

@ -240,18 +240,26 @@ public class AbpLocalization_Tests : AbpIntegratedTest<AbpLocalizationTestModule
using (CultureHelper.Use(CultureInfo.GetCultureInfo("de")))
{
_localizer["Car"].Value.ShouldBe("Auto");
}
using (CultureHelper.Use(CultureInfo.GetCultureInfo("de")))
{
_localizer["CarPlural"].Value.ShouldBe("Autos");
}
using (CultureHelper.Use(CultureInfo.GetCultureInfo("de")))
{
_localizer["Biography"].Value.ShouldBe("Biografie");
_localizer["Enum:BookType.Undefined"].Value.ShouldBe("Nicht definiert");
}
}
[Fact]
public void GetAllStrings_Should_Contain_Values_From_All_Split_Files()
{
using (CultureHelper.Use(CultureInfo.GetCultureInfo("de")))
{
_localizer["Enum:BookType.Undefined"].Value.ShouldBe("Nicht definiert");
var allStrings = _localizer.GetAllStrings(false).ToList();
// From de.json
allStrings.ShouldContain(ls => ls.Name == "Car" && ls.Value == "Auto");
allStrings.ShouldContain(ls => ls.Name == "FortyTwo" && ls.Value == "Zweiundvierzig");
// From de_Book.json
allStrings.ShouldContain(ls => ls.Name == "Biography" && ls.Value == "Biografie");
allStrings.ShouldContain(ls => ls.Name == "Enum:BookType.Undefined" && ls.Value == "Nicht definiert");
}
}

40
framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpStringLocalizerFactory_Tests.cs

@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.Extensions.Localization;
using Shouldly;
using Volo.Abp.DynamicProxy;
@ -92,7 +93,7 @@ public class AbpStringLocalizerFactory_Tests : AbpIntegratedTest<AbpLocalization
}
[Fact]
public async Task Should_Create_Resource_By_Name_FromSplitFiles()
public void Should_Create_Resource_By_Name_FromSplitFiles()
{
using (CultureHelper.Use("en"))
{
@ -103,4 +104,37 @@ public class AbpStringLocalizerFactory_Tests : AbpIntegratedTest<AbpLocalization
localizer["ThisIsRequiredValue"].Value.ShouldBe("This is required value (this will be used)");
}
}
}
[Fact]
public void Should_Get_All_Strings_From_Split_Files()
{
using (CultureHelper.Use("en"))
{
var localizer = _factory.CreateByResourceNameOrNull("LocalizationTestFilesSplit");
localizer.ShouldNotBeNull();
var allStrings = localizer.GetAllStrings().ToList();
allStrings.ShouldContain(ls => ls.Name == "Base.Id" && ls.Value == "Id");
allStrings.ShouldContain(ls => ls.Name == "Base.Name" && ls.Value == "Name");
allStrings.ShouldContain(ls => ls.Name == "Book.Id" && ls.Value == "ISBN");
allStrings.ShouldContain(ls => ls.Name == "Book.Name" && ls.Value == "Title");
allStrings.ShouldContain(ls => ls.Name == "ThisIsRequiredValue" && ls.Value == "This is required value (this will be used)");
}
}
[Fact]
public void Should_Override_Value_From_Later_Split_File()
{
using (CultureHelper.Use("en"))
{
var localizer = _factory.CreateByResourceNameOrNull("LocalizationTestFilesSplit");
localizer.ShouldNotBeNull();
// !en_First.json defines "This is required value"
// zen_Last.json defines "This is required value (this will be used)"
// zen_Last.json sorts after !en_First.json (ordinal: '!' < 'z'), so its value wins
localizer["ThisIsRequiredValue"].Value.ShouldBe("This is required value (this will be used)");
}
}
}

47
framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/JsonLocalizationDictionaryBuilder_Tests.cs

@ -1,10 +1,5 @@
using System;
using System.Collections.Generic;
using System.Text;
using NSubstitute.ExceptionExtensions;
using Shouldly;
using Shouldly;
using Volo.Abp.Localization.Json;
using Volo.Abp.Testing;
using Xunit;
namespace Volo.Abp.Localization;
@ -12,14 +7,28 @@ namespace Volo.Abp.Localization;
/// <summary>
/// Testing edge cases for <see cref="JsonLocalizationDictionaryBuilder"/>
/// </summary>
public class JsonLocalizationDictionaryBuilder_Tests : AbpIntegratedTest<AbpLocalizationTestModule>
public class JsonLocalizationDictionaryBuilder_Tests
{
[Fact]
public void JsonLocalizationDictionaryBuilder_Should_Handle_Duplicates()
{
var localizationDictionary = JsonLocalizationDictionaryBuilder
.BuildFromJsonString("{\r\n \"culture\": \"en\",\r\n \"texts\": {\r\n \"ThisFieldIsRequired\": \"This field is required\",\r\n \"MaxLenghtErrorMessage\": \"This field can be maximum of '{0}' chars\",\r\n \"Enum:BookType.Undefined\": \"Undefined from ValidationResource\",\r\n \"Enum:BookType.0\": \"Undefined with value 0 from ValidationResource\",\r\n \"BookType.Adventure\": \"Adventure from ValidationResource\",\r\n \"BookType.1\": \"Adventure with value 1 from ValidationResource\",\r\n \"Biography\": \"Biography from ValidationResource\",\r\n \"ThisFieldIsRequired\": \"This field is required again\"\r\n }\r\n}");
var input = """
{
"culture": "en",
"texts": {
"ThisFieldIsRequired": "This field is required",
"MaxLenghtErrorMessage": "This field can be maximum of '{0}' chars",
"Enum:BookType.Undefined": "Undefined from ValidationResource",
"Enum:BookType.0": "Undefined with value 0 from ValidationResource",
"BookType.Adventure": "Adventure from ValidationResource",
"BookType.1": "Adventure with value 1 from ValidationResource",
"Biography": "Biography from ValidationResource",
"ThisFieldIsRequired": "This field is required again"
}
}
""";
var localizationDictionary = JsonLocalizationDictionaryBuilder.BuildFromJsonString(input);
var localizationString = localizationDictionary.GetOrNull("ThisFieldIsRequired");
localizationString.ShouldNotBeNull();
@ -29,18 +38,20 @@ public class JsonLocalizationDictionaryBuilder_Tests : AbpIntegratedTest<AbpLoca
[Fact]
public void JsonLocalizationDictionaryBuilder_Should_Handle_Deep_Duplicates()
{
var input = @"{
""culture"": ""en"",
""texts"": {
""ThisFieldIsRequired"": ""This field is required"",
""DeepLocaliaztionKey"": {""DeepKey"": ""DeepValue""},
""DeepLocaliaztionKey__DeepKey"": ""Another translation""
}
}";
var input = """
{
"culture": "en",
"texts": {
"ThisFieldIsRequired": "This field is required",
"DeepLocalizationKey": { "DeepKey": "DeepValue" },
"DeepLocalizationKey__DeepKey": "Another translation"
}
}
""";
var localizationDictionary = JsonLocalizationDictionaryBuilder.BuildFromJsonString(input);
localizationDictionary.ShouldNotBeNull();
var localizationString = localizationDictionary.GetOrNull("DeepLocaliaztionKey__DeepKey");
var localizationString = localizationDictionary.GetOrNull("DeepLocalizationKey__DeepKey");
localizationString.ShouldNotBeNull();
localizationString.Value.ShouldBe("Another translation");
}

Loading…
Cancel
Save