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 diff --git a/vueJs/src/App.vue b/vueJs/src/App.vue index 87e0b786d..70c48f0d9 100644 --- a/vueJs/src/App.vue +++ b/vueJs/src/App.vue @@ -17,14 +17,14 @@ import ServiceWorkerUpdatePopup from '@/pwa/components/ServiceWorkerUpdatePopup. ServiceWorkerUpdatePopup } }) -export default class extends Vue { +export default class App extends Vue { created() { - this.initializeAbpConfiguration() + this.initialize() } - private async initializeAbpConfiguration() { + private async initialize() { await HttpProxyModule.Initialize() - await AbpModule.LoadAbpConfiguration() + await AbpModule.Initialize() } } diff --git a/vueJs/src/api/dynamic-api.ts b/vueJs/src/api/dynamic-api.ts index c6aac5d7a..2e0d028da 100644 --- a/vueJs/src/api/dynamic-api.ts +++ b/vueJs/src/api/dynamic-api.ts @@ -207,44 +207,12 @@ export class ControllerApiDescriptionModel { type!: string interfaces = new Array() actions: {[key: string]: ActionApiDescriptionModel} = {} - - public static getAction( - actionName: string, - actions: {[key: string]: ActionApiDescriptionModel}) { - const actionKeys = Object.keys(actions) - const index = actionKeys.findIndex(key => { - const a = actions[key] - if (a.name.toLowerCase() === actionName.toLowerCase()) { - return a - } - }) - if (index < 0) { - throw new Error(`没有找到名为 ${actionName} 的方法定义!`) - } - return actions[actionKeys[index]] - } } export class ModuleApiDescriptionModel { rootPath = 'app' remoteServiceName = 'Default' controllers: {[key: string]: ControllerApiDescriptionModel} = {} - - public static getController( - controllerName: string, - controllers: {[key: string]: ControllerApiDescriptionModel}) { - const controllerKeys = Object.keys(controllers) - const index = controllerKeys.findIndex(key => { - const c = controllers[key] - if (c.controllerName.toLowerCase() === controllerName.toLowerCase()) { - return c - } - }) - if (index < 0) { - throw new Error(`没有找到名为 ${controllerName} 的接口定义!`) - } - return controllers[controllerKeys[index]] - } } export class TypeApiDescriptionModel { @@ -259,30 +227,4 @@ export class TypeApiDescriptionModel { export class ApplicationApiDescriptionModel { modules: {[key: string]: ModuleApiDescriptionModel} = {} types: {[key: string]: TypeApiDescriptionModel} = {} - - public static getAction( - remoteService: string, - controllerName: string, - actionName: string, - modules: {[key: string]: ModuleApiDescriptionModel}) { - const module = this.getModule(remoteService, modules) - const controller = ModuleApiDescriptionModel.getController(controllerName, module.controllers) - return ControllerApiDescriptionModel.getAction(actionName, controller.actions) - } - - private static getModule( - remoteService: string, - modules: {[key: string]: ModuleApiDescriptionModel}) { - const moduleKeys = Object.keys(modules) - const index = moduleKeys.findIndex(key => { - const m = modules[key] - if (m.remoteServiceName.toLowerCase() === remoteService.toLowerCase()) { - return m - } - }) - if (index < 0) { - throw new Error(`没有找到名为 ${remoteService} 的服务定义!`) - } - return modules[moduleKeys[index]] - } } diff --git a/vueJs/src/components/LangSelect/index.vue b/vueJs/src/components/LangSelect/index.vue index 8031383aa..f9e572c46 100644 --- a/vueJs/src/components/LangSelect/index.vue +++ b/vueJs/src/components/LangSelect/index.vue @@ -40,7 +40,7 @@ export default class extends Vue { AppModule.SetLanguage(lang) this.$i18n.locale = lang this.localization.currentCulture.cultureName = lang - AbpModule.LoadAbpConfiguration().then(() => { + AbpModule.Initialize().then(() => { this.$message({ message: 'Switch Language Success', type: 'success' diff --git a/vueJs/src/components/TenantBox/index.vue b/vueJs/src/components/TenantBox/index.vue index 64c5e856b..4600fc01b 100644 --- a/vueJs/src/components/TenantBox/index.vue +++ b/vueJs/src/components/TenantBox/index.vue @@ -49,7 +49,7 @@ export default class extends Vue { }) } else { AbpModule.configuration.currentTenant.isAvailable = false - AbpModule.LoadAbpConfiguration().finally(() => { + AbpModule.Initialize().finally(() => { this.$emit('input', '') }) } diff --git a/vueJs/src/main.ts b/vueJs/src/main.ts index a59ee9adb..a57a79654 100644 --- a/vueJs/src/main.ts +++ b/vueJs/src/main.ts @@ -59,6 +59,7 @@ Object.keys(filters).forEach(key => { }) Vue.config.productionTip = false +Vue.prototype.$r = {} new Vue({ router, diff --git a/vueJs/src/mixins/HttpProxyMiXin.ts b/vueJs/src/mixins/HttpProxyMiXin.ts index 6db5325ff..5482342e2 100644 --- a/vueJs/src/mixins/HttpProxyMiXin.ts +++ b/vueJs/src/mixins/HttpProxyMiXin.ts @@ -7,7 +7,9 @@ import { ApiVersionInfo, ParameterBindingSources, UrlBuilder, - ApplicationApiDescriptionModel + ApplicationApiDescriptionModel, + ModuleApiDescriptionModel, + ControllerApiDescriptionModel } from '@/api/dynamic-api' /** * 动态Http代理组件 @@ -16,6 +18,8 @@ import { name: 'HttpProxyMiXin' }) export default class HttpProxyMiXin extends Vue { + private apiDescriptor = HttpProxyModule.applicationApiDescriptionModel + protected pagedRequest(options: { service: string, controller: string, @@ -32,10 +36,9 @@ export default class HttpProxyMiXin extends Vue { action: string, data?: any }) { - const action = ApplicationApiDescriptionModel - .getAction( - options.service, options.controller, options.action, - HttpProxyModule.applicationApiDescriptionModel.modules) + const module = this.getModule(options.service, this.apiDescriptor.modules) + const controller = this.getController(options.controller, module.controllers) + const action = this.getAction(options.action, controller.actions) const apiVersion = this.getApiVersionInfo(action) let url = process.env.REMOTE_SERVICE_BASE_URL || '' url = this.ensureEndsWith(url, '/') + UrlBuilder.generateUrlWithParameters(action, options.data, apiVersion) @@ -47,6 +50,54 @@ export default class HttpProxyMiXin extends Vue { }) } + private getModule( + remoteService: string, + modules: {[key: string]: ModuleApiDescriptionModel}) { + const moduleKeys = Object.keys(modules) + const index = moduleKeys.findIndex(key => { + const m = modules[key] + if (m.remoteServiceName.toLowerCase() === remoteService.toLowerCase()) { + return m + } + }) + if (index < 0) { + throw new Error(`没有找到名为 ${remoteService} 的服务定义!`) + } + return modules[moduleKeys[index]] + } + + private getController( + controllerName: string, + controllers: {[key: string]: ControllerApiDescriptionModel}) { + const controllerKeys = Object.keys(controllers) + const index = controllerKeys.findIndex(key => { + const c = controllers[key] + if (c.controllerName.toLowerCase() === controllerName.toLowerCase()) { + return c + } + }) + if (index < 0) { + throw new Error(`没有找到名为 ${controllerName} 的接口定义!`) + } + return controllers[controllerKeys[index]] + } + + private getAction( + actionName: string, + actions: {[key: string]: ActionApiDescriptionModel}) { + const actionKeys = Object.keys(actions) + const index = actionKeys.findIndex(key => { + const a = actions[key] + if (a.name.toLowerCase() === actionName.toLowerCase()) { + return a + } + }) + if (index < 0) { + throw new Error(`没有找到名为 ${actionName} 的方法定义!`) + } + return actions[actionKeys[index]] + } + private getApiVersionInfo(action: ActionApiDescriptionModel) { const apiVersion = this.findBestApiVersion(action) const versionParam = diff --git a/vueJs/src/store/modules/abp.ts b/vueJs/src/store/modules/abp.ts index 1d9700d86..dda886e9e 100644 --- a/vueJs/src/store/modules/abp.ts +++ b/vueJs/src/store/modules/abp.ts @@ -27,7 +27,7 @@ class AbpConfiguration extends VuexModule implements IAbpState { } @Action({ rawError: true }) - public LoadAbpConfiguration() { + public Initialize() { return new Promise((resolve, reject) => { AbpConfigurationService.getAbpConfiguration().then(config => { this.SET_ABPCONFIGURATION(config) diff --git a/vueJs/src/store/modules/http-proxy.ts b/vueJs/src/store/modules/http-proxy.ts index cd3abf0b2..f9307e385 100644 --- a/vueJs/src/store/modules/http-proxy.ts +++ b/vueJs/src/store/modules/http-proxy.ts @@ -22,25 +22,16 @@ class HttpProxy extends VuexModule implements IHttpProxy { public async Initialize() { const dynamicApiJson = getItem(dynamicApiKey) if (dynamicApiJson) { - this.SET_API_DESCRIPTOR(JSON.parse(dynamicApiJson)) - return + const apiDescriptor = JSON.parse(dynamicApiJson) as ApplicationApiDescriptionModel + if (apiDescriptor) { + this.SET_API_DESCRIPTOR(apiDescriptor) + return + } } const dynamicApi = await DynamicApiService.get() this.SET_API_DESCRIPTOR(dynamicApi) setItem(dynamicApiKey, JSON.stringify(dynamicApi)) } - - @Action({ rawError: true }) - public findAction(service: { - name: string, - controller: string, - action: string - }) { - return ApplicationApiDescriptionModel - .getAction( - service.name, service.controller, service.action, - this.applicationApiDescriptionModel.modules) - } } export const HttpProxyModule = getModule(HttpProxy) diff --git a/vueJs/src/store/modules/user.ts b/vueJs/src/store/modules/user.ts index 9d8d96865..81a136311 100644 --- a/vueJs/src/store/modules/user.ts +++ b/vueJs/src/store/modules/user.ts @@ -136,7 +136,7 @@ class User extends VuexModule implements IUserState { @Action private async PostLogin() { - const abpConfig = await AbpModule.LoadAbpConfiguration() + const abpConfig = await AbpModule.Initialize() this.SET_CURRENTUSERINFO(abpConfig.currentUser) } } diff --git a/vueJs/src/views/admin/identityServer/persisted-grants/index.vue b/vueJs/src/views/admin/identityServer/persisted-grants/index.vue index c243cced2..b85414450 100644 --- a/vueJs/src/views/admin/identityServer/persisted-grants/index.vue +++ b/vueJs/src/views/admin/identityServer/persisted-grants/index.vue @@ -35,7 +35,7 @@ :label="$t('AbpIdentityServer.Client:Id')" prop="clientId" sortable - width="150px" + width="180px" >