diff --git a/aspnet-core/LINGYUN.MicroService.All.sln b/aspnet-core/LINGYUN.MicroService.All.sln
index c3e00e3aa..21536b9a5 100644
--- a/aspnet-core/LINGYUN.MicroService.All.sln
+++ b/aspnet-core/LINGYUN.MicroService.All.sln
@@ -290,7 +290,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Location.Tencen
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Location.Baidu.Tests", "tests\LINGYUN.Abp.Location.Baidu.Tests\LINGYUN.Abp.Location.Baidu.Tests.csproj", "{C892CD81-50AE-49E5-BF44-A0C28A1614CC}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.AspNetCore.Mvc.Client", "modules\common\LINGYUN.Abp.AspNetCore.Mvc.Client\LINGYUN.Abp.AspNetCore.Mvc.Client.csproj", "{EEF03CC6-1013-4AAF-BEED-BB4BA5021039}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.AspNetCore.Mvc.Client", "modules\common\LINGYUN.Abp.AspNetCore.Mvc.Client\LINGYUN.Abp.AspNetCore.Mvc.Client.csproj", "{EEF03CC6-1013-4AAF-BEED-BB4BA5021039}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "localization", "localization", "{90E88EAC-4291-4406-8D88-EFDF61B11292}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Localization.Xml", "modules\localization\LINGYUN.Abp.Localization.Xml\LINGYUN.Abp.Localization.Xml.csproj", "{84868710-ECBB-4025-900A-EEB99EC49534}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Localization.Xml.Tests", "tests\LINGYUN.Abp.Localization.Xml.Tests\LINGYUN.Abp.Localization.Xml.Tests.csproj", "{A061D2B4-B650-4F7F-A6CB-5C8FFFD512ED}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Localization.Json", "modules\localization\LINGYUN.Abp.Localization.Json\LINGYUN.Abp.Localization.Json.csproj", "{EA563F48-A6EF-4886-B607-2A83F7795F1B}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LINGYUN.Abp.Localization.Json.Tests", "tests\LINGYUN.Abp.Localization.Json.Tests\LINGYUN.Abp.Localization.Json.Tests.csproj", "{EBCF7D88-49E2-413D-A7A6-1A76BC2E8161}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -770,6 +780,22 @@ Global
{EEF03CC6-1013-4AAF-BEED-BB4BA5021039}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EEF03CC6-1013-4AAF-BEED-BB4BA5021039}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EEF03CC6-1013-4AAF-BEED-BB4BA5021039}.Release|Any CPU.Build.0 = Release|Any CPU
+ {84868710-ECBB-4025-900A-EEB99EC49534}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {84868710-ECBB-4025-900A-EEB99EC49534}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {84868710-ECBB-4025-900A-EEB99EC49534}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {84868710-ECBB-4025-900A-EEB99EC49534}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A061D2B4-B650-4F7F-A6CB-5C8FFFD512ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A061D2B4-B650-4F7F-A6CB-5C8FFFD512ED}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A061D2B4-B650-4F7F-A6CB-5C8FFFD512ED}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A061D2B4-B650-4F7F-A6CB-5C8FFFD512ED}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EA563F48-A6EF-4886-B607-2A83F7795F1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EA563F48-A6EF-4886-B607-2A83F7795F1B}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EA563F48-A6EF-4886-B607-2A83F7795F1B}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EA563F48-A6EF-4886-B607-2A83F7795F1B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EBCF7D88-49E2-413D-A7A6-1A76BC2E8161}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EBCF7D88-49E2-413D-A7A6-1A76BC2E8161}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EBCF7D88-49E2-413D-A7A6-1A76BC2E8161}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EBCF7D88-49E2-413D-A7A6-1A76BC2E8161}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -915,6 +941,11 @@ Global
{94B47385-E47F-4FD7-A3A9-A7AA122EFC93} = {370D7CD5-1E17-4F3D-BBFA-03429F6D4F2F}
{C892CD81-50AE-49E5-BF44-A0C28A1614CC} = {370D7CD5-1E17-4F3D-BBFA-03429F6D4F2F}
{EEF03CC6-1013-4AAF-BEED-BB4BA5021039} = {8AC72641-30D3-4ACF-89FA-808FADC55C2E}
+ {90E88EAC-4291-4406-8D88-EFDF61B11292} = {C5CAD011-DF84-4914-939C-0C029DCEF26F}
+ {84868710-ECBB-4025-900A-EEB99EC49534} = {90E88EAC-4291-4406-8D88-EFDF61B11292}
+ {A061D2B4-B650-4F7F-A6CB-5C8FFFD512ED} = {370D7CD5-1E17-4F3D-BBFA-03429F6D4F2F}
+ {EA563F48-A6EF-4886-B607-2A83F7795F1B} = {90E88EAC-4291-4406-8D88-EFDF61B11292}
+ {EBCF7D88-49E2-413D-A7A6-1A76BC2E8161} = {370D7CD5-1E17-4F3D-BBFA-03429F6D4F2F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C95FDF91-16F2-4A8B-A4BE-0E62D1B66718}
diff --git a/aspnet-core/LINGYUN.MicroService.Common.sln b/aspnet-core/LINGYUN.MicroService.Common.sln
index f106c402f..3e8024941 100644
--- a/aspnet-core/LINGYUN.MicroService.Common.sln
+++ b/aspnet-core/LINGYUN.MicroService.Common.sln
@@ -117,6 +117,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Location.Tencen
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.AspNetCore.Mvc.Client", "modules\common\LINGYUN.Abp.AspNetCore.Mvc.Client\LINGYUN.Abp.AspNetCore.Mvc.Client.csproj", "{7F767ACF-754A-4EBC-8936-3C1402B6EF82}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "localization", "localization", "{E73A0F8B-2B4B-4CED-82A4-1EE5E0B89744}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Localization.Json", "modules\localization\LINGYUN.Abp.Localization.Json\LINGYUN.Abp.Localization.Json.csproj", "{DADD5D6E-F09A-4563-A659-7922E26C40AB}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Localization.Xml", "modules\localization\LINGYUN.Abp.Localization.Xml\LINGYUN.Abp.Localization.Xml.csproj", "{8CC72F4E-F134-4A43-9037-5D4D1F29B68A}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Localization.Xml.Tests", "tests\LINGYUN.Abp.Localization.Xml.Tests\LINGYUN.Abp.Localization.Xml.Tests.csproj", "{94FEA59E-3B6D-41A0-9E44-BA5D6477244F}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LINGYUN.Abp.Localization.Json.Tests", "tests\LINGYUN.Abp.Localization.Json.Tests\LINGYUN.Abp.Localization.Json.Tests.csproj", "{BA2F4EC9-BC2C-482A-9123-BDACB8B15295}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -307,6 +317,22 @@ Global
{7F767ACF-754A-4EBC-8936-3C1402B6EF82}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7F767ACF-754A-4EBC-8936-3C1402B6EF82}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7F767ACF-754A-4EBC-8936-3C1402B6EF82}.Release|Any CPU.Build.0 = Release|Any CPU
+ {DADD5D6E-F09A-4563-A659-7922E26C40AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {DADD5D6E-F09A-4563-A659-7922E26C40AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {DADD5D6E-F09A-4563-A659-7922E26C40AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {DADD5D6E-F09A-4563-A659-7922E26C40AB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8CC72F4E-F134-4A43-9037-5D4D1F29B68A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8CC72F4E-F134-4A43-9037-5D4D1F29B68A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8CC72F4E-F134-4A43-9037-5D4D1F29B68A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8CC72F4E-F134-4A43-9037-5D4D1F29B68A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {94FEA59E-3B6D-41A0-9E44-BA5D6477244F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {94FEA59E-3B6D-41A0-9E44-BA5D6477244F}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {94FEA59E-3B6D-41A0-9E44-BA5D6477244F}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {94FEA59E-3B6D-41A0-9E44-BA5D6477244F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BA2F4EC9-BC2C-482A-9123-BDACB8B15295}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BA2F4EC9-BC2C-482A-9123-BDACB8B15295}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BA2F4EC9-BC2C-482A-9123-BDACB8B15295}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BA2F4EC9-BC2C-482A-9123-BDACB8B15295}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -367,6 +393,11 @@ Global
{221725FF-6C01-4F41-9F29-AC04C7D52611} = {B86C21A4-73B7-471E-B73A-B4B905EC9435}
{1B494EA1-28CF-4A61-B0BE-70BBA425C316} = {B86C21A4-73B7-471E-B73A-B4B905EC9435}
{7F767ACF-754A-4EBC-8936-3C1402B6EF82} = {086BE5BE-8594-4DA7-8819-935FEF76DABD}
+ {E73A0F8B-2B4B-4CED-82A4-1EE5E0B89744} = {02EA4E78-5891-43BC-944F-3E52FEE032E4}
+ {DADD5D6E-F09A-4563-A659-7922E26C40AB} = {E73A0F8B-2B4B-4CED-82A4-1EE5E0B89744}
+ {8CC72F4E-F134-4A43-9037-5D4D1F29B68A} = {E73A0F8B-2B4B-4CED-82A4-1EE5E0B89744}
+ {94FEA59E-3B6D-41A0-9E44-BA5D6477244F} = {B86C21A4-73B7-471E-B73A-B4B905EC9435}
+ {BA2F4EC9-BC2C-482A-9123-BDACB8B15295} = {B86C21A4-73B7-471E-B73A-B4B905EC9435}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {06C707C6-02C0-411A-AD3B-2D0E13787CB8}
diff --git a/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Json/LINGYUN.Abp.Localization.Json.csproj b/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Json/LINGYUN.Abp.Localization.Json.csproj
new file mode 100644
index 000000000..76ab204bf
--- /dev/null
+++ b/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Json/LINGYUN.Abp.Localization.Json.csproj
@@ -0,0 +1,15 @@
+
+
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Json/LINGYUN/Abp/Localization/Json/AbpLocalizationJsonModule.cs b/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Json/LINGYUN/Abp/Localization/Json/AbpLocalizationJsonModule.cs
new file mode 100644
index 000000000..b2a4672cc
--- /dev/null
+++ b/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Json/LINGYUN/Abp/Localization/Json/AbpLocalizationJsonModule.cs
@@ -0,0 +1,10 @@
+using Volo.Abp.Localization;
+using Volo.Abp.Modularity;
+
+namespace LINGYUN.Abp.Localization.Json
+{
+ [DependsOn(typeof(AbpLocalizationModule))]
+ public class AbpLocalizationJsonModule : AbpModule
+ {
+ }
+}
diff --git a/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Json/LINGYUN/Abp/Localization/Json/JsonPhysicalFileLocalizationResourceContributor.cs b/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Json/LINGYUN/Abp/Localization/Json/JsonPhysicalFileLocalizationResourceContributor.cs
new file mode 100644
index 000000000..1b862f4b8
--- /dev/null
+++ b/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Json/LINGYUN/Abp/Localization/Json/JsonPhysicalFileLocalizationResourceContributor.cs
@@ -0,0 +1,116 @@
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Localization;
+using Microsoft.Extensions.Primitives;
+using System;
+using System.Collections.Generic;
+using Volo.Abp;
+using Volo.Abp.Internal;
+using Volo.Abp.Localization;
+using Volo.Abp.Localization.Json;
+
+namespace LINGYUN.Abp.Localization.Json
+{
+ public class JsonPhysicalFileLocalizationResourceContributor : ILocalizationResourceContributor
+ {
+ private readonly string _filePath;
+
+ private IFileProvider _fileProvider;
+ private Dictionary _dictionaries;
+ private bool _subscribedForChanges;
+ private readonly object _syncObj = new object();
+
+ public JsonPhysicalFileLocalizationResourceContributor(string filePath)
+ {
+ _filePath = filePath;
+ }
+
+ public void Initialize(LocalizationResourceInitializationContext context)
+ {
+ _fileProvider = new PhysicalFileProvider(_filePath);
+ }
+
+ public LocalizedString GetOrNull(string cultureName, string name)
+ {
+ return GetDictionaries().GetOrDefault(cultureName)?.GetOrNull(name);
+ }
+
+ public void Fill(string cultureName, Dictionary dictionary)
+ {
+ GetDictionaries().GetOrDefault(cultureName)?.Fill(dictionary);
+ }
+
+ protected virtual Dictionary GetDictionaries()
+ {
+ var dictionaries = _dictionaries;
+ if (dictionaries != null)
+ {
+ return dictionaries;
+ }
+
+ lock (_syncObj)
+ {
+ dictionaries = _dictionaries;
+ if (dictionaries != null)
+ {
+ return dictionaries;
+ }
+
+ if (!_subscribedForChanges)
+ {
+ ChangeToken.OnChange(() => _fileProvider.Watch(_filePath.EnsureEndsWith('/') + "*.*"),
+ () =>
+ {
+ _dictionaries = null;
+ });
+
+ _subscribedForChanges = true;
+ }
+
+ dictionaries = _dictionaries = CreateDictionaries();
+ }
+
+ return dictionaries;
+ }
+
+ protected virtual Dictionary CreateDictionaries()
+ {
+ var dictionaries = new Dictionary();
+
+ foreach (var file in _fileProvider.GetDirectoryContents(string.Empty))
+ {
+ if (file.IsDirectory || !CanParseFile(file))
+ {
+ continue;
+ }
+
+ var dictionary = CreateDictionaryFromFile(file);
+ if (dictionaries.ContainsKey(dictionary.CultureName))
+ {
+ throw new AbpException($"{file.GetVirtualOrPhysicalPathOrNull()} dictionary has a culture name '{dictionary.CultureName}' which is already defined!");
+ }
+
+ dictionaries[dictionary.CultureName] = dictionary;
+ }
+
+ return dictionaries;
+ }
+
+ protected virtual bool CanParseFile(IFileInfo file)
+ {
+ return file.Name.EndsWith(".json", StringComparison.OrdinalIgnoreCase);
+ }
+
+ protected virtual ILocalizationDictionary CreateDictionaryFromFile(IFileInfo file)
+ {
+ using (var stream = file.CreateReadStream())
+ {
+ return CreateDictionaryFromFileContent(Utf8Helper.ReadStringFromStream(stream));
+ }
+ }
+
+ protected virtual ILocalizationDictionary CreateDictionaryFromFileContent(string fileContent)
+ {
+ return JsonLocalizationDictionaryBuilder.BuildFromJsonString(fileContent);
+ }
+ }
+}
diff --git a/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Json/LINGYUN/Abp/Localization/Json/LocalizationResourceExtensions.cs b/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Json/LINGYUN/Abp/Localization/Json/LocalizationResourceExtensions.cs
new file mode 100644
index 000000000..a662d5dcf
--- /dev/null
+++ b/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Json/LINGYUN/Abp/Localization/Json/LocalizationResourceExtensions.cs
@@ -0,0 +1,27 @@
+using JetBrains.Annotations;
+using Volo.Abp;
+using Volo.Abp.Localization;
+
+namespace LINGYUN.Abp.Localization.Json
+{
+ public static class LocalizationResourceExtensions
+ {
+ ///
+ /// 添加Json本地磁盘文件支持
+ ///
+ ///
+ ///
+ ///
+ public static LocalizationResource AddPhysicalJson(
+ [NotNull] this LocalizationResource localizationResource,
+ [NotNull] string jsonFilePath)
+ {
+ Check.NotNull(localizationResource, nameof(localizationResource));
+ Check.NotNull(jsonFilePath, nameof(jsonFilePath));
+
+ localizationResource.Contributors.Add(new JsonPhysicalFileLocalizationResourceContributor(jsonFilePath));
+
+ return localizationResource;
+ }
+ }
+}
diff --git a/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Json/README.md b/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Json/README.md
new file mode 100644
index 000000000..038608d48
--- /dev/null
+++ b/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Json/README.md
@@ -0,0 +1,55 @@
+# LINGYUN.Abp.Localization.Json
+
+## 模块说明
+
+本地化组件的Json本地文件系统集成,Abp内置组件仅集成了基于IVirtualFileProvider的实现
+
+此组件基于PhysicalFileProvider
+
+### 基础模块
+
+### 高阶模块
+
+### 权限定义
+
+### 功能定义
+
+### 配置定义
+
+### 如何使用
+
+
+```csharp
+
+ [DependsOn(
+ typeof(AbpLocalizationJsonModule))]
+ public class YouProjectModule : AbpModule
+ {
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ Configure(options =>
+ {
+ options.Resources
+ .Add("en")
+ // 一般配置在宿主项目中, 写入宿主项目中存储json文件的绝对路径(受PhysicalFileProvider的限制)
+ // json文件格式与Abp默认json格式保持一致
+ // 详情见: https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.fileproviders.physicalfileprovider?view=dotnet-plat-ext-5.0
+ .AddPhysicalJson(Path.Combine(Directory.GetCurrentDirectory(), "Resources"));
+ });
+ }
+ }
+
+```
+
+```json
+
+{
+ "culture": "en",
+ "texts": {
+ "Hello China": "Hello China!"
+ }
+}
+
+```
+
+### 更新日志
diff --git a/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Xml/LINGYUN.Abp.Localization.Xml.csproj b/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Xml/LINGYUN.Abp.Localization.Xml.csproj
new file mode 100644
index 000000000..da94801ce
--- /dev/null
+++ b/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Xml/LINGYUN.Abp.Localization.Xml.csproj
@@ -0,0 +1,15 @@
+
+
+
+
+
+ netstandard2.0
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Xml/LINGYUN/Abp/Localization/Xml/AbpLocalizationXmlModule.cs b/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Xml/LINGYUN/Abp/Localization/Xml/AbpLocalizationXmlModule.cs
new file mode 100644
index 000000000..60d9bd316
--- /dev/null
+++ b/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Xml/LINGYUN/Abp/Localization/Xml/AbpLocalizationXmlModule.cs
@@ -0,0 +1,10 @@
+using Volo.Abp.Localization;
+using Volo.Abp.Modularity;
+
+namespace LINGYUN.Abp.Localization.Xml
+{
+ [DependsOn(typeof(AbpLocalizationModule))]
+ public class AbpLocalizationXmlModule : AbpModule
+ {
+ }
+}
diff --git a/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Xml/LINGYUN/Abp/Localization/Xml/LocalizationResourceExtensions.cs b/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Xml/LINGYUN/Abp/Localization/Xml/LocalizationResourceExtensions.cs
new file mode 100644
index 000000000..d6b90bfe9
--- /dev/null
+++ b/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Xml/LINGYUN/Abp/Localization/Xml/LocalizationResourceExtensions.cs
@@ -0,0 +1,47 @@
+using JetBrains.Annotations;
+using System;
+using Volo.Abp;
+using Volo.Abp.Localization;
+
+namespace LINGYUN.Abp.Localization.Xml
+{
+ public static class LocalizationResourceExtensions
+ {
+ ///
+ /// 添加Xml虚拟文件系统支持
+ ///
+ ///
+ ///
+ ///
+ public static LocalizationResource AddVirtualXml(
+ [NotNull] this LocalizationResource localizationResource,
+ [NotNull] string virtualPath)
+ {
+ Check.NotNull(localizationResource, nameof(localizationResource));
+ Check.NotNull(virtualPath, nameof(virtualPath));
+
+ localizationResource.Contributors.Add(new XmlVirtualFileLocalizationResourceContributor(
+ virtualPath.EnsureStartsWith('/')
+ ));
+
+ return localizationResource;
+ }
+ ///
+ /// 添加Xml本地磁盘文件支持
+ ///
+ ///
+ ///
+ ///
+ public static LocalizationResource AddPhysicalXml(
+ [NotNull] this LocalizationResource localizationResource,
+ [NotNull] string xmlFilePath)
+ {
+ Check.NotNull(localizationResource, nameof(localizationResource));
+ Check.NotNull(xmlFilePath, nameof(xmlFilePath));
+
+ localizationResource.Contributors.Add(new XmlPhysicalFileLocalizationResourceContributor(xmlFilePath));
+
+ return localizationResource;
+ }
+ }
+}
diff --git a/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Xml/LINGYUN/Abp/Localization/Xml/XmlFileLocalizationResourceContributorBase.cs b/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Xml/LINGYUN/Abp/Localization/Xml/XmlFileLocalizationResourceContributorBase.cs
new file mode 100644
index 000000000..e49f42e4b
--- /dev/null
+++ b/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Xml/LINGYUN/Abp/Localization/Xml/XmlFileLocalizationResourceContributorBase.cs
@@ -0,0 +1,119 @@
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Localization;
+using Microsoft.Extensions.Primitives;
+using System;
+using System.Collections.Generic;
+using Volo.Abp;
+using Volo.Abp.Internal;
+using Volo.Abp.Localization;
+
+namespace LINGYUN.Abp.Localization.Xml
+{
+ public abstract class XmlFileLocalizationResourceContributorBase : ILocalizationResourceContributor
+ {
+ private readonly string _filePath;
+
+ private IFileProvider _fileProvider;
+ private Dictionary _dictionaries;
+ private bool _subscribedForChanges;
+ private readonly object _syncObj = new object();
+
+ protected XmlFileLocalizationResourceContributorBase(string filePath)
+ {
+ _filePath = filePath;
+ }
+
+ public virtual void Initialize(LocalizationResourceInitializationContext context)
+ {
+ _fileProvider = BuildFileProvider(context);
+
+ Check.NotNull(_fileProvider, nameof(_fileProvider));
+ }
+
+ public virtual LocalizedString GetOrNull(string cultureName, string name)
+ {
+ return GetDictionaries().GetOrDefault(cultureName)?.GetOrNull(name);
+ }
+
+ public virtual void Fill(string cultureName, Dictionary dictionary)
+ {
+ GetDictionaries().GetOrDefault(cultureName)?.Fill(dictionary);
+ }
+
+ protected virtual Dictionary GetDictionaries()
+ {
+ var dictionaries = _dictionaries;
+ if (dictionaries != null)
+ {
+ return dictionaries;
+ }
+
+ lock (_syncObj)
+ {
+ dictionaries = _dictionaries;
+ if (dictionaries != null)
+ {
+ return dictionaries;
+ }
+
+ if (!_subscribedForChanges)
+ {
+ ChangeToken.OnChange(() => _fileProvider.Watch(_filePath.EnsureEndsWith('/') + "*.*"),
+ () =>
+ {
+ _dictionaries = null;
+ });
+
+ _subscribedForChanges = true;
+ }
+
+ dictionaries = _dictionaries = CreateDictionaries(_fileProvider, _filePath);
+ }
+
+ return dictionaries;
+ }
+
+ protected virtual Dictionary CreateDictionaries(IFileProvider fileProvider, string filePath)
+ {
+ var dictionaries = new Dictionary();
+
+ foreach (var file in fileProvider.GetDirectoryContents(filePath))
+ {
+ if (file.IsDirectory || !CanParseFile(file))
+ {
+ continue;
+ }
+
+ var dictionary = CreateDictionaryFromFile(file);
+ if (dictionaries.ContainsKey(dictionary.CultureName))
+ {
+ throw new AbpException($"{file.GetVirtualOrPhysicalPathOrNull()} dictionary has a culture name '{dictionary.CultureName}' which is already defined!");
+ }
+
+ dictionaries[dictionary.CultureName] = dictionary;
+ }
+
+ return dictionaries;
+ }
+
+ protected virtual bool CanParseFile(IFileInfo file)
+ {
+ return file.Name.EndsWith(".xml", StringComparison.OrdinalIgnoreCase);
+ }
+
+ protected virtual ILocalizationDictionary CreateDictionaryFromFile(IFileInfo file)
+ {
+ using (var stream = file.CreateReadStream())
+ {
+ return CreateDictionaryFromFileContent(Utf8Helper.ReadStringFromStream(stream));
+ }
+ }
+
+ protected virtual ILocalizationDictionary CreateDictionaryFromFileContent(string fileContent)
+ {
+ return XmlLocalizationDictionaryBuilder.BuildFromXmlString(fileContent);
+ }
+
+ protected abstract IFileProvider BuildFileProvider(LocalizationResourceInitializationContext context);
+ }
+}
diff --git a/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Xml/LINGYUN/Abp/Localization/Xml/XmlLocalizationDictionaryBuilder.cs b/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Xml/LINGYUN/Abp/Localization/Xml/XmlLocalizationDictionaryBuilder.cs
new file mode 100644
index 000000000..f3dbb712e
--- /dev/null
+++ b/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Xml/LINGYUN/Abp/Localization/Xml/XmlLocalizationDictionaryBuilder.cs
@@ -0,0 +1,74 @@
+using Microsoft.Extensions.Localization;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml.Serialization;
+using Volo.Abp;
+using Volo.Abp.Localization;
+
+namespace LINGYUN.Abp.Localization.Xml
+{
+ public static class XmlLocalizationDictionaryBuilder
+ {
+ public static ILocalizationDictionary BuildFromFile(string filePath)
+ {
+ try
+ {
+ return BuildFromXmlString(File.ReadAllText(filePath));
+ }
+ catch (Exception ex)
+ {
+ throw new AbpException("Invalid localization file format: " + filePath, ex);
+ }
+ }
+
+ public static ILocalizationDictionary BuildFromXmlString(string xmlString)
+ {
+ XmlLocalizationFile xmlFile;
+ try
+ {
+ XmlSerializer serializer = new XmlSerializer(typeof(XmlLocalizationFile));
+ using (StringReader reader = new StringReader(xmlString))
+ {
+ xmlFile = (XmlLocalizationFile)serializer.Deserialize(reader);
+ }
+ }
+ catch (Exception ex)
+ {
+ throw new AbpException("Can not parse xml string. " + ex.Message);
+ }
+
+ var cultureCode = xmlFile.Culture.Name;
+ if (string.IsNullOrEmpty(cultureCode))
+ {
+ throw new AbpException("Culture is empty in language json file.");
+ }
+
+ var dictionary = new Dictionary();
+ var dublicateNames = new List();
+ foreach (var item in xmlFile.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());
+ }
+
+ if (dublicateNames.Count > 0)
+ {
+ throw new AbpException(
+ "A dictionary can not contain same key twice. There are some duplicated names: " +
+ dublicateNames.JoinAsString(", "));
+ }
+
+ return new StaticLocalizationDictionary(cultureCode, dictionary);
+ }
+ }
+}
diff --git a/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Xml/LINGYUN/Abp/Localization/Xml/XmlLocalizationFile.cs b/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Xml/LINGYUN/Abp/Localization/Xml/XmlLocalizationFile.cs
new file mode 100644
index 000000000..b2f93373e
--- /dev/null
+++ b/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Xml/LINGYUN/Abp/Localization/Xml/XmlLocalizationFile.cs
@@ -0,0 +1,98 @@
+using System;
+using System.IO;
+using System.Text;
+using System.Xml;
+using System.Xml.Serialization;
+
+namespace LINGYUN.Abp.Localization.Xml
+{
+ [Serializable]
+ [XmlRoot(Namespace = "lingyun.abp", ElementName = "localization")]
+ public class XmlLocalizationFile
+ {
+ [XmlElement("culture")]
+ public CultureInfo Culture { get; set; } = new CultureInfo();
+
+ [XmlArray("texts")]
+ [XmlArrayItem("text")]
+ public LocalizationText[] Texts { get; set; } = new LocalizationText[0];
+
+ public XmlLocalizationFile() { }
+
+ public XmlLocalizationFile(string culture)
+ {
+ Culture = new CultureInfo(culture);
+ }
+
+ public XmlLocalizationFile(string culture, LocalizationText[] texts)
+ {
+ Culture = new CultureInfo(culture);
+ Texts = texts;
+ }
+
+ public void WriteToPath(string filePath)
+ {
+ var fileName = Path.Combine(filePath, Culture.Name + ".xml");
+ using FileStream fileStream = new(fileName, FileMode.Create, FileAccess.Write);
+ InternalWrite(fileStream, Encoding.UTF8);
+ }
+
+ private void InternalWrite(Stream stream, Encoding encoding)
+ {
+ if (encoding == null)
+ {
+ throw new ArgumentNullException("encoding");
+ }
+
+ XmlSerializer serializer = new(GetType());
+ XmlWriterSettings settings = new()
+ {
+ Indent = true,
+ NewLineChars = "\r\n",
+ Encoding = encoding,
+ IndentChars = " "
+ };
+
+ using XmlWriter writer = XmlWriter.Create(stream, settings);
+ serializer.Serialize(writer, this);
+ writer.Close();
+ }
+ }
+
+ [Serializable]
+ public class CultureInfo
+ {
+ [XmlAttribute("name")]
+ public string Name { get; set; }
+
+ public CultureInfo()
+ {
+ }
+
+ public CultureInfo(string name)
+ {
+ Name = name;
+ }
+ }
+
+ [Serializable]
+ public class LocalizationText
+ {
+ [XmlAttribute("key")]
+ public string Key { get; set; }
+
+ [XmlAttribute("value")]
+ public string Value { get; set; }
+
+ public LocalizationText()
+ {
+
+ }
+
+ public LocalizationText(string key, string value)
+ {
+ Key = key;
+ Value = value;
+ }
+ }
+}
diff --git a/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Xml/LINGYUN/Abp/Localization/Xml/XmlPhysicalFileLocalizationResourceContributor.cs b/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Xml/LINGYUN/Abp/Localization/Xml/XmlPhysicalFileLocalizationResourceContributor.cs
new file mode 100644
index 000000000..fc3161bfa
--- /dev/null
+++ b/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Xml/LINGYUN/Abp/Localization/Xml/XmlPhysicalFileLocalizationResourceContributor.cs
@@ -0,0 +1,46 @@
+using Microsoft.Extensions.FileProviders;
+using System.Collections.Generic;
+using Volo.Abp;
+using Volo.Abp.Localization;
+
+namespace LINGYUN.Abp.Localization.Xml
+{
+ public class XmlPhysicalFileLocalizationResourceContributor : XmlFileLocalizationResourceContributorBase
+ {
+ private readonly string _filePath;
+
+ public XmlPhysicalFileLocalizationResourceContributor(string filePath)
+ : base(filePath)
+ {
+ _filePath = filePath;
+ }
+
+ protected override IFileProvider BuildFileProvider(LocalizationResourceInitializationContext context)
+ {
+ return new PhysicalFileProvider(_filePath);
+ }
+
+ protected override Dictionary CreateDictionaries(IFileProvider fileProvider, string filePath)
+ {
+ var dictionaries = new Dictionary();
+
+ foreach (var file in fileProvider.GetDirectoryContents(string.Empty))
+ {
+ if (file.IsDirectory || !CanParseFile(file))
+ {
+ continue;
+ }
+
+ var dictionary = CreateDictionaryFromFile(file);
+ if (dictionaries.ContainsKey(dictionary.CultureName))
+ {
+ throw new AbpException($"{file.GetVirtualOrPhysicalPathOrNull()} dictionary has a culture name '{dictionary.CultureName}' which is already defined!");
+ }
+
+ dictionaries[dictionary.CultureName] = dictionary;
+ }
+
+ return dictionaries;
+ }
+ }
+}
diff --git a/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Xml/LINGYUN/Abp/Localization/Xml/XmlVirtualFileLocalizationResourceContributor.cs b/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Xml/LINGYUN/Abp/Localization/Xml/XmlVirtualFileLocalizationResourceContributor.cs
new file mode 100644
index 000000000..e6ced1dfe
--- /dev/null
+++ b/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Xml/LINGYUN/Abp/Localization/Xml/XmlVirtualFileLocalizationResourceContributor.cs
@@ -0,0 +1,20 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.FileProviders;
+using Volo.Abp.Localization;
+using Volo.Abp.VirtualFileSystem;
+
+namespace LINGYUN.Abp.Localization.Xml
+{
+ public class XmlVirtualFileLocalizationResourceContributor : XmlFileLocalizationResourceContributorBase
+ {
+ public XmlVirtualFileLocalizationResourceContributor(string filePath)
+ : base(filePath)
+ {
+ }
+
+ protected override IFileProvider BuildFileProvider(LocalizationResourceInitializationContext context)
+ {
+ return context.ServiceProvider.GetRequiredService();
+ }
+ }
+}
diff --git a/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Xml/README.md b/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Xml/README.md
new file mode 100644
index 000000000..9c810b056
--- /dev/null
+++ b/aspnet-core/modules/localization/LINGYUN.Abp.Localization.Xml/README.md
@@ -0,0 +1,64 @@
+# LINGYUN.Abp.Localization.Xml
+
+## 模块说明
+
+本地化组件的Xml文档集成,内置PhysicalFileProvider与VirtualFileProvider实现
+
+### 基础模块
+
+### 高阶模块
+
+### 权限定义
+
+### 功能定义
+
+### 配置定义
+
+### 如何使用
+
+
+```csharp
+
+ [DependsOn(
+ typeof(AbpLocalizationXmlModule))]
+ public class YouProjectModule : AbpModule
+ {
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ Configure(options =>
+ {
+ options.FileSets.AddEmbedded();
+ });
+
+ Configure(options =>
+ {
+ options.Resources
+ .Add("en")
+ // 当前项目中的虚拟文件系统目录,详情见: https://docs.abp.io/en/abp/latest/Virtual-File-System
+ .AddVirtualXml("/LINGYUN/Abp/Localization/Xml/Resources")
+ // 一般配置在宿主项目中, 写入宿主项目中存储xml文件的绝对路径(受PhysicalFileProvider的限制)
+ // 详情见: https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.fileproviders.physicalfileprovider?view=dotnet-plat-ext-5.0
+ .AddPhysicalXml(Path.Combine(Directory.GetCurrentDirectory(), "Resources"));
+ });
+ }
+ }
+
+```
+Xml文件格式如下
+
+序列化: [XmlLocalizationFile](./LINGYUN/Abp/Localization/Xml/XmlLocalizationFile.cs) 类型实现
+
+```xml
+
+
+
+
+
+
+
+
+
+
+```
+
+### 更新日志
diff --git a/aspnet-core/services/account/AuthServer.Host/AuthIdentityServerModule.cs b/aspnet-core/services/account/AuthServer.Host/AuthIdentityServerModule.cs
index 30c2737f5..e3d1737f1 100644
--- a/aspnet-core/services/account/AuthServer.Host/AuthIdentityServerModule.cs
+++ b/aspnet-core/services/account/AuthServer.Host/AuthIdentityServerModule.cs
@@ -19,7 +19,9 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using StackExchange.Redis;
using System;
+using System.IO;
using System.Linq;
+using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Text.Encodings.Web;
using System.Text.Unicode;
@@ -41,6 +43,7 @@ using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore.MySQL;
using Volo.Abp.FeatureManagement.EntityFrameworkCore;
using Volo.Abp.Identity;
+using Volo.Abp.IdentityServer;
using Volo.Abp.IdentityServer.Jwt;
using Volo.Abp.Json;
using Volo.Abp.Json.SystemTextJson;
@@ -89,6 +92,7 @@ namespace AuthServer.Host
public override void PreConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
+ var hostingEnvironment = context.Services.GetHostingEnvironment();
PreConfigure(options =>
{
@@ -100,6 +104,29 @@ namespace AuthServer.Host
})
.UseDashboard();
});
+
+ var cerConfig = configuration.GetSection("Certificates");
+ if (hostingEnvironment.IsProduction() &&
+ cerConfig.Exists())
+ {
+ // 开发环境下存在证书配置
+ // 且证书文件存在则使用自定义的证书文件来启动Ids服务器
+ var cerPath = Path.Combine(hostingEnvironment.ContentRootPath, cerConfig["CerPath"]);
+ if (File.Exists(cerPath))
+ {
+ PreConfigure(options =>
+ {
+ options.AddDeveloperSigningCredential = false;
+ });
+
+ var cer = new X509Certificate2(cerPath, cerConfig["Password"]);
+
+ PreConfigure(builder =>
+ {
+ builder.AddSigningCredential(cer);
+ });
+ }
+ }
}
public override void ConfigureServices(ServiceConfigurationContext context)
@@ -280,6 +307,7 @@ namespace AuthServer.Host
}
else
{
+ // 需要实现一个错误页面
app.UseErrorPage();
app.UseHsts();
}
diff --git a/aspnet-core/tests/LINGYUN.Abp.Localization.Json.Tests/LINGYUN.Abp.Localization.Json.Tests.csproj b/aspnet-core/tests/LINGYUN.Abp.Localization.Json.Tests/LINGYUN.Abp.Localization.Json.Tests.csproj
new file mode 100644
index 000000000..6aa92b3f2
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.Localization.Json.Tests/LINGYUN.Abp.Localization.Json.Tests.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net5.0
+
+ false
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/tests/LINGYUN.Abp.Localization.Json.Tests/LINGYUN/Abp/Localization/Json/AbpLocalizationJsonTestBase.cs b/aspnet-core/tests/LINGYUN.Abp.Localization.Json.Tests/LINGYUN/Abp/Localization/Json/AbpLocalizationJsonTestBase.cs
new file mode 100644
index 000000000..d00ba4aed
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.Localization.Json.Tests/LINGYUN/Abp/Localization/Json/AbpLocalizationJsonTestBase.cs
@@ -0,0 +1,8 @@
+using LINGYUN.Abp.Tests;
+
+namespace LINGYUN.Abp.Localization.Json
+{
+ public abstract class AbpLocalizationJsonTestBase : AbpTestsBase
+ {
+ }
+}
diff --git a/aspnet-core/tests/LINGYUN.Abp.Localization.Json.Tests/LINGYUN/Abp/Localization/Json/AbpLocalizationJsonTestModule.cs b/aspnet-core/tests/LINGYUN.Abp.Localization.Json.Tests/LINGYUN/Abp/Localization/Json/AbpLocalizationJsonTestModule.cs
new file mode 100644
index 000000000..fcd3c15c4
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.Localization.Json.Tests/LINGYUN/Abp/Localization/Json/AbpLocalizationJsonTestModule.cs
@@ -0,0 +1,23 @@
+using LINGYUN.Abp.Tests;
+using System.IO;
+using Volo.Abp.Localization;
+using Volo.Abp.Modularity;
+
+namespace LINGYUN.Abp.Localization.Json
+{
+ [DependsOn(
+ typeof(AbpTestsBaseModule),
+ typeof(AbpLocalizationJsonModule))]
+ public class AbpLocalizationJsonTestModule : AbpModule
+ {
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ Configure(options =>
+ {
+ options.Resources
+ .Add("en")
+ .AddPhysicalJson(Path.Combine(Directory.GetCurrentDirectory(), "TestResources"));
+ });
+ }
+ }
+}
diff --git a/aspnet-core/tests/LINGYUN.Abp.Localization.Json.Tests/LINGYUN/Abp/Localization/Json/JsonLocalizationTest.cs b/aspnet-core/tests/LINGYUN.Abp.Localization.Json.Tests/LINGYUN/Abp/Localization/Json/JsonLocalizationTest.cs
new file mode 100644
index 000000000..6dd1b8c84
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.Localization.Json.Tests/LINGYUN/Abp/Localization/Json/JsonLocalizationTest.cs
@@ -0,0 +1,32 @@
+using Microsoft.Extensions.Localization;
+using Shouldly;
+using Volo.Abp.Localization;
+using Xunit;
+
+namespace LINGYUN.Abp.Localization.Json
+{
+ public class JsonLocalizationTest : AbpLocalizationJsonTestBase
+ {
+ private readonly IStringLocalizer _localizer;
+
+ public JsonLocalizationTest()
+ {
+ _localizer = GetRequiredService>();
+ }
+
+
+ [Fact]
+ public void Should_Get_Physical_Localized_Text_If_Defined_In_Current_Culture()
+ {
+ using (CultureHelper.Use("zh-Hans"))
+ {
+ _localizer["Hello China"].Value.ShouldBe("China 你好!");
+ }
+
+ using (CultureHelper.Use("en"))
+ {
+ _localizer["Hello China"].Value.ShouldBe("Hello China!");
+ }
+ }
+ }
+}
diff --git a/aspnet-core/tests/LINGYUN.Abp.Localization.Json.Tests/LINGYUN/Abp/Localization/Json/LocalizationTestResource.cs b/aspnet-core/tests/LINGYUN.Abp.Localization.Json.Tests/LINGYUN/Abp/Localization/Json/LocalizationTestResource.cs
new file mode 100644
index 000000000..209adf0c7
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.Localization.Json.Tests/LINGYUN/Abp/Localization/Json/LocalizationTestResource.cs
@@ -0,0 +1,6 @@
+namespace LINGYUN.Abp.Localization.Json
+{
+ public class LocalizationTestResource
+ {
+ }
+}
diff --git a/aspnet-core/tests/LINGYUN.Abp.Localization.Json.Tests/TestResources/en.json b/aspnet-core/tests/LINGYUN.Abp.Localization.Json.Tests/TestResources/en.json
new file mode 100644
index 000000000..843f59c19
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.Localization.Json.Tests/TestResources/en.json
@@ -0,0 +1,6 @@
+{
+ "culture": "en",
+ "texts": {
+ "Hello China": "Hello China!"
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/tests/LINGYUN.Abp.Localization.Json.Tests/TestResources/zh-Hans.json b/aspnet-core/tests/LINGYUN.Abp.Localization.Json.Tests/TestResources/zh-Hans.json
new file mode 100644
index 000000000..a60bd9936
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.Localization.Json.Tests/TestResources/zh-Hans.json
@@ -0,0 +1,6 @@
+{
+ "culture": "zh-Hans",
+ "texts": {
+ "Hello China": "China 你好!"
+ }
+}
\ No newline at end of file
diff --git a/aspnet-core/tests/LINGYUN.Abp.Localization.Xml.Tests/LINGYUN.Abp.Localization.Xml.Tests.csproj b/aspnet-core/tests/LINGYUN.Abp.Localization.Xml.Tests/LINGYUN.Abp.Localization.Xml.Tests.csproj
new file mode 100644
index 000000000..acce4cd0c
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.Localization.Xml.Tests/LINGYUN.Abp.Localization.Xml.Tests.csproj
@@ -0,0 +1,37 @@
+
+
+
+ net5.0
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
+
diff --git a/aspnet-core/tests/LINGYUN.Abp.Localization.Xml.Tests/LINGYUN/Abp/Localization/Xml/AbpLocalizationXmlTestBase.cs b/aspnet-core/tests/LINGYUN.Abp.Localization.Xml.Tests/LINGYUN/Abp/Localization/Xml/AbpLocalizationXmlTestBase.cs
new file mode 100644
index 000000000..b2c7cbd89
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.Localization.Xml.Tests/LINGYUN/Abp/Localization/Xml/AbpLocalizationXmlTestBase.cs
@@ -0,0 +1,8 @@
+using LINGYUN.Abp.Tests;
+
+namespace LINGYUN.Abp.Localization.Xml
+{
+ public abstract class AbpLocalizationXmlTestBase : AbpTestsBase
+ {
+ }
+}
diff --git a/aspnet-core/tests/LINGYUN.Abp.Localization.Xml.Tests/LINGYUN/Abp/Localization/Xml/AbpLocalizationXmlTestModule.cs b/aspnet-core/tests/LINGYUN.Abp.Localization.Xml.Tests/LINGYUN/Abp/Localization/Xml/AbpLocalizationXmlTestModule.cs
new file mode 100644
index 000000000..2c5023832
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.Localization.Xml.Tests/LINGYUN/Abp/Localization/Xml/AbpLocalizationXmlTestModule.cs
@@ -0,0 +1,30 @@
+using LINGYUN.Abp.Tests;
+using System.IO;
+using Volo.Abp.Localization;
+using Volo.Abp.Modularity;
+using Volo.Abp.VirtualFileSystem;
+
+namespace LINGYUN.Abp.Localization.Xml
+{
+ [DependsOn(
+ typeof(AbpTestsBaseModule),
+ typeof(AbpLocalizationXmlModule))]
+ public class AbpLocalizationXmlTestModule : AbpModule
+ {
+ public override void ConfigureServices(ServiceConfigurationContext context)
+ {
+ Configure(options =>
+ {
+ options.FileSets.AddEmbedded();
+ });
+
+ Configure(options =>
+ {
+ options.Resources
+ .Add("en")
+ .AddVirtualXml("/LINGYUN/Abp/Localization/Xml/Resources")
+ .AddPhysicalXml(Path.Combine(Directory.GetCurrentDirectory(), "TestResources"));
+ });
+ }
+ }
+}
diff --git a/aspnet-core/tests/LINGYUN.Abp.Localization.Xml.Tests/LINGYUN/Abp/Localization/Xml/LocalizationTestResource.cs b/aspnet-core/tests/LINGYUN.Abp.Localization.Xml.Tests/LINGYUN/Abp/Localization/Xml/LocalizationTestResource.cs
new file mode 100644
index 000000000..f66b21c59
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.Localization.Xml.Tests/LINGYUN/Abp/Localization/Xml/LocalizationTestResource.cs
@@ -0,0 +1,6 @@
+namespace LINGYUN.Abp.Localization.Xml
+{
+ public class LocalizationTestResource
+ {
+ }
+}
diff --git a/aspnet-core/tests/LINGYUN.Abp.Localization.Xml.Tests/LINGYUN/Abp/Localization/Xml/Resources/en.xml b/aspnet-core/tests/LINGYUN.Abp.Localization.Xml.Tests/LINGYUN/Abp/Localization/Xml/Resources/en.xml
new file mode 100644
index 000000000..54ca06f6f
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.Localization.Xml.Tests/LINGYUN/Abp/Localization/Xml/Resources/en.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/tests/LINGYUN.Abp.Localization.Xml.Tests/LINGYUN/Abp/Localization/Xml/Resources/zh-Hans.xml b/aspnet-core/tests/LINGYUN.Abp.Localization.Xml.Tests/LINGYUN/Abp/Localization/Xml/Resources/zh-Hans.xml
new file mode 100644
index 000000000..560893002
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.Localization.Xml.Tests/LINGYUN/Abp/Localization/Xml/Resources/zh-Hans.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/aspnet-core/tests/LINGYUN.Abp.Localization.Xml.Tests/LINGYUN/Abp/Localization/Xml/XmlLocalizationTest.cs b/aspnet-core/tests/LINGYUN.Abp.Localization.Xml.Tests/LINGYUN/Abp/Localization/Xml/XmlLocalizationTest.cs
new file mode 100644
index 000000000..8d1500989
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.Localization.Xml.Tests/LINGYUN/Abp/Localization/Xml/XmlLocalizationTest.cs
@@ -0,0 +1,94 @@
+using Microsoft.Extensions.Localization;
+using Shouldly;
+using System.IO;
+using Volo.Abp.Localization;
+using Xunit;
+
+namespace LINGYUN.Abp.Localization.Xml
+{
+ public class XmlLocalizationTest : AbpLocalizationXmlTestBase
+ {
+ private readonly static string[] _localizerFiles = new string[]
+ {
+ Path.Combine("./TestResources", "zh-Hans.xml"),
+ Path.Combine("./TestResources", "en.xml")
+ };
+
+ private readonly IStringLocalizer _localizer;
+
+ public XmlLocalizationTest()
+ {
+ _localizer = GetRequiredService>();
+ }
+
+ [Fact]
+ public void Should_Get_Physical_Localized_Text_If_Defined_In_Current_Culture()
+ {
+ Init();
+
+ using (CultureHelper.Use("zh-Hans"))
+ {
+ _localizer["Hello World"].Value.ShouldBe("世界你好!");
+ _localizer["C# Test"].Value.ShouldBe("C#测试");
+ }
+
+ using (CultureHelper.Use("en"))
+ {
+ _localizer["Hello World"].Value.ShouldBe("Hello World!");
+ _localizer["C# Test"].Value.ShouldBe("C# Test!");
+ }
+ }
+
+ [Fact]
+ public void Should_Get_Virtual_Localized_Text_If_Defined_In_Current_Culture()
+ {
+ using (CultureHelper.Use("zh-Hans"))
+ {
+ _localizer["Hello China"].Value.ShouldBe("China 你好!");
+ }
+
+ using (CultureHelper.Use("en"))
+ {
+ _localizer["Hello China"].Value.ShouldBe("Hello China!");
+ }
+ }
+
+ private static void Init()
+ {
+ var zhHansfile = new XmlLocalizationFile("zh-Hans")
+ {
+ Texts = new LocalizationText[]
+ {
+ new LocalizationText("Hello World", "世界你好!"),
+ new LocalizationText("C# Test", "C#测试")
+ }
+ };
+
+ zhHansfile.WriteToPath("./TestResources");
+
+ var enFile = new XmlLocalizationFile("en")
+ {
+ Texts = new LocalizationText[]
+ {
+ new LocalizationText("Hello World", "Hello World!"),
+ new LocalizationText("C# Test", "C# Test!")
+ }
+ };
+
+ enFile.WriteToPath("./TestResources");
+ }
+
+ public override void Dispose()
+ {
+ base.Dispose();
+
+ foreach (var file in _localizerFiles)
+ {
+ if (File.Exists(file))
+ {
+ File.Delete(file);
+ }
+ }
+ }
+ }
+}
diff --git a/aspnet-core/tests/LINGYUN.Abp.Localization.Xml.Tests/TestResources/zh-Hans.xml b/aspnet-core/tests/LINGYUN.Abp.Localization.Xml.Tests/TestResources/zh-Hans.xml
new file mode 100644
index 000000000..61b962d50
--- /dev/null
+++ b/aspnet-core/tests/LINGYUN.Abp.Localization.Xml.Tests/TestResources/zh-Hans.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file