Browse Source

Merge pull request #23701 from nebula2/18208-nested-l8n

feat(l8n): add support for nested objects in localization files
pull/23713/head
Ma Liming 5 months ago
committed by GitHub
parent
commit
e6a51b1410
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 28
      docs/en/framework/fundamentals/localization.md
  2. 72
      framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Json/JsonLocalizationDictionaryBuilder.cs
  3. 7
      framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Json/JsonLocalizationFile.cs
  4. 25
      framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/AbpLocalization_Tests.cs
  5. 24
      framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/SourceExt/en.json

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

@ -91,6 +91,34 @@ A JSON localization file content is shown below:
> ABP will ignore (skip) the JSON file if the `culture` section is missing.
You can also use nesting or array in localization files, like this:
````json
{
"culture": "en",
"texts": {
"HelloWorld": "Hello World!",
"Hello": {
"World": "Hello World!"
},
"Hi":[
"Bye": "Bye World!"
"Hello": "Hello World!"
]
}
}
````
Then you can use it like this:
> The double underscore (`__`) is used to separate the parent key from the child key.
````csharp
var str = L["Hello__World"]; // Hello World!
var str2 = L["Hi__0"]; // Bye World!
var str3 = L["Hi__1"]; // Hello World!
````
### Default Resource
`AbpLocalizationOptions.DefaultResourceType` can be set to a resource type, so it is used when the localization resource was not specified:

72
framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Json/JsonLocalizationDictionaryBuilder.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using Microsoft.Extensions.Localization;
@ -47,12 +48,11 @@ public static class JsonLocalizationDictionaryBuilder
{
throw new AbpException("Can not parse json string. " + ex.Message);
}
if (jsonFile == null)
{
return null;
}
var cultureCode = jsonFile.Culture;
if (string.IsNullOrEmpty(cultureCode))
{
@ -61,18 +61,17 @@ public static class JsonLocalizationDictionaryBuilder
var dictionary = new Dictionary<string, LocalizedString>();
var dublicateNames = new List<string>();
foreach (var item in jsonFile.Texts)
foreach (var item in FlattenTexts(jsonFile.Texts))
{
if (string.IsNullOrEmpty(item.Key))
{
throw new AbpException("The key is empty in given json string.");
}
if (dictionary.GetOrDefault(item.Key) != null)
{
dublicateNames.Add(item.Key);
}
dictionary[item.Key] = new LocalizedString(item.Key, item.Value.NormalizeLineEndings());
}
@ -85,4 +84,67 @@ public static class JsonLocalizationDictionaryBuilder
return new StaticLocalizationDictionary(cultureCode, dictionary);
}
private static Dictionary<string, string> FlattenTexts(Dictionary<string, object> texts, string prefix = "")
{
var result = new Dictionary<string, string>();
foreach (var text in texts)
{
var currentKey = string.IsNullOrEmpty(prefix) ? text.Key : $"{prefix}__{text.Key}";
switch (text.Value)
{
case JsonElement jsonElement:
foreach (var item in FlattenJsonElement(jsonElement, currentKey))
{
result[item.Key] = item.Value;
}
break;
case string str:
result[currentKey] = str;
break;
case null:
result[currentKey] = "";
break;
default:
result[currentKey] = text.Value.ToString() ?? "";
break;
}
}
return result;
}
private static IEnumerable<KeyValuePair<string, string>> FlattenJsonElement(JsonElement element, string prefix)
{
switch (element.ValueKind)
{
case JsonValueKind.String:
yield return new KeyValuePair<string, string>(prefix, element.GetString() ?? "");
break;
case JsonValueKind.Object:
foreach (var prop in element.EnumerateObject())
{
var newKey = $"{prefix}__{prop.Name}";
foreach (var item in FlattenJsonElement(prop.Value, newKey))
{
yield return item;
}
}
break;
case JsonValueKind.Array:
var i = 0;
foreach (var prop in element.EnumerateArray())
{
var newKey = $"{prefix}__{i}";
foreach (var item in FlattenJsonElement(prop, newKey))
{
yield return item;
}
i++;
}
break;
default:
yield return new KeyValuePair<string, string>(prefix, element.ToString());
break;
}
}
}

7
framework/src/Volo.Abp.Localization/Volo/Abp/Localization/Json/JsonLocalizationFile.cs

@ -9,10 +9,5 @@ public class JsonLocalizationFile
/// </summary>
public string Culture { get; set; } = default!;
public Dictionary<string, string> Texts { get; set; }
public JsonLocalizationFile()
{
Texts = new Dictionary<string, string>();
}
public Dictionary<string, object> Texts { get; set; } = [];
}

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

@ -196,7 +196,7 @@ public class AbpLocalization_Tests : AbpIntegratedTest<AbpLocalizationTestModule
{
_localizer["CarPlural"].Value.ShouldBe("汽车");
}
using (CultureHelper.Use(CultureInfo.GetCultureInfo("zh-Hans-CN")))
{
_localizer["Car"].Value.ShouldBe("汽车");
@ -214,7 +214,7 @@ public class AbpLocalization_Tests : AbpIntegratedTest<AbpLocalizationTestModule
{
_localizer["CarPlural"].Value.ShouldBe("汽車");
}
using (CultureHelper.Use(CultureInfo.GetCultureInfo("zh-TW")))
{
_localizer["Car"].Value.ShouldBe("汽車");
@ -223,7 +223,7 @@ public class AbpLocalization_Tests : AbpIntegratedTest<AbpLocalizationTestModule
{
_localizer["CarPlural"].Value.ShouldBe("汽車");
}
using (CultureHelper.Use(CultureInfo.GetCultureInfo("zh-Hant-TW")))
{
_localizer["Car"].Value.ShouldBe("汽車");
@ -375,4 +375,21 @@ public class AbpLocalization_Tests : AbpIntegratedTest<AbpLocalizationTestModule
var externalLocalizer = _localizerFactory.CreateByResourceName(TestExternalLocalizationStore.TestExternalResourceNames.ExternalResource1);
externalLocalizer["Car"].Value.ShouldBe("Car");
}
}
[Fact]
public void Should_Get_Nested_Translations()
{
using (CultureHelper.Use("en"))
{
_localizer["MyNestedTranslation__SomeKey"].Value.ShouldBe("Some nested value");
_localizer["MyNestedTranslation__SomeOtherKey"].Value.ShouldBe("Some other nested value");
_localizer["MyNestedTranslation__DeeplyNested__DeepKey"].Value.ShouldBe("A deeply nested value");
_localizer["MyNestedTranslation__DeeplyNestedArray__0"].Value.ShouldBe("First value in array");
_localizer["MyNestedTranslation__DeeplyNestedArray__1"].Value.ShouldBe("Second value in array");
_localizer["MyNestedTranslation__DeeplyNestedArray__2__InnerDeepKey"].Value.ShouldBe("Inner deeply nested value");
_localizer["MyNestedTranslation__DeeplyNestedArray__3__InnerDeepArray__0"].Value.ShouldBe("First inner deep array value");
_localizer["MyNestedTranslation__DeeplyNestedArray__3__InnerDeepArray__1"].Value.ShouldBe("Second inner deep array value");
}
}
}

24
framework/test/Volo.Abp.Localization.Tests/Volo/Abp/Localization/TestResources/SourceExt/en.json

@ -1,6 +1,26 @@
{
"culture": "en",
"texts": {
"SeeYou": "See you"
"SeeYou": "See you",
"MyNestedTranslation": {
"SomeKey": "Some nested value",
"SomeOtherKey": "Some other nested value",
"DeeplyNested": {
"DeepKey": "A deeply nested value"
},
"DeeplyNestedArray": [
"First value in array",
"Second value in array",
{
"InnerDeepKey": "Inner deeply nested value"
},
{
"InnerDeepArray": [
"First inner deep array value",
"Second inner deep array value"
]
}
]
}
}
}
}

Loading…
Cancel
Save