From 07bd7d5c87f766963294f8cd3ed3619f359f4565 Mon Sep 17 00:00:00 2001 From: whuanle <1586052146@qq.com> Date: Sat, 9 Dec 2023 10:00:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E5=A2=9E=E5=8A=A0abp=E7=9A=84i18n?= =?UTF-8?q?=E6=9C=AC=E5=9C=B0=E5=8C=96=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- FreeRedis.sln | 62 ++++++++++++ examples/abpapi_net8/ApiModule.cs | 42 +++++++++ examples/abpapi_net8/Program.cs | 49 ++++++++++ .../Properties/launchSettings.json | 12 +++ examples/abpapi_net8/TestResource.cs | 6 ++ examples/abpapi_net8/abpapi_net8.csproj | 18 ++++ examples/abpapi_net8_benchmark/Ben.cs | 48 ++++++++++ examples/abpapi_net8_benchmark/Program.cs | 14 +++ .../PublishProfiles/FolderProfile.pubxml | 13 +++ .../abpapi_net8_benchmark.csproj | 20 ++++ examples/abpapi_net8_server/AppModule.cs | 80 ++++++++++++++++ .../Controllers/IndexController.cs | 28 ++++++ examples/abpapi_net8_server/Program.cs | 17 ++++ .../Properties/launchSettings.json | 31 ++++++ examples/abpapi_net8_server/TestResource.cs | 6 ++ .../abpapi_net8_server.csproj | 19 ++++ .../abpapi_net8_server.http | 6 ++ .../appsettings.Development.json | 8 ++ examples/abpapi_net8_server/appsettings.json | 9 ++ .../FreeRedis.Abp.Localization/Extensions.cs | 46 +++++++++ .../FreeRedis.Abp.Localization.csproj | 17 ++++ .../FreeRedisLocalizationResourceBase.cs | 41 ++++++++ .../FreeRedisLocalizationResourceFactory.cs | 88 ++++++++++++++++++ .../LanguageOptions.cs | 42 +++++++++ nuget/FreeRedis.Abp.Localization/README.md | 78 ++++++++++++++++ .../images/image-20231209085550710.png | Bin 0 -> 6989 bytes .../images/image-20231209085649631.png | Bin 0 -> 13253 bytes .../images/image-20231209085657772.png | Bin 0 -> 14123 bytes 28 files changed, 800 insertions(+) create mode 100644 examples/abpapi_net8/ApiModule.cs create mode 100644 examples/abpapi_net8/Program.cs create mode 100644 examples/abpapi_net8/Properties/launchSettings.json create mode 100644 examples/abpapi_net8/TestResource.cs create mode 100644 examples/abpapi_net8/abpapi_net8.csproj create mode 100644 examples/abpapi_net8_benchmark/Ben.cs create mode 100644 examples/abpapi_net8_benchmark/Program.cs create mode 100644 examples/abpapi_net8_benchmark/Properties/PublishProfiles/FolderProfile.pubxml create mode 100644 examples/abpapi_net8_benchmark/abpapi_net8_benchmark.csproj create mode 100644 examples/abpapi_net8_server/AppModule.cs create mode 100644 examples/abpapi_net8_server/Controllers/IndexController.cs create mode 100644 examples/abpapi_net8_server/Program.cs create mode 100644 examples/abpapi_net8_server/Properties/launchSettings.json create mode 100644 examples/abpapi_net8_server/TestResource.cs create mode 100644 examples/abpapi_net8_server/abpapi_net8_server.csproj create mode 100644 examples/abpapi_net8_server/abpapi_net8_server.http create mode 100644 examples/abpapi_net8_server/appsettings.Development.json create mode 100644 examples/abpapi_net8_server/appsettings.json create mode 100644 nuget/FreeRedis.Abp.Localization/Extensions.cs create mode 100644 nuget/FreeRedis.Abp.Localization/FreeRedis.Abp.Localization.csproj create mode 100644 nuget/FreeRedis.Abp.Localization/FreeRedisLocalizationResourceBase.cs create mode 100644 nuget/FreeRedis.Abp.Localization/FreeRedisLocalizationResourceFactory.cs create mode 100644 nuget/FreeRedis.Abp.Localization/LanguageOptions.cs create mode 100644 nuget/FreeRedis.Abp.Localization/README.md create mode 100644 nuget/FreeRedis.Abp.Localization/images/image-20231209085550710.png create mode 100644 nuget/FreeRedis.Abp.Localization/images/image-20231209085649631.png create mode 100644 nuget/FreeRedis.Abp.Localization/images/image-20231209085657772.png diff --git a/FreeRedis.sln b/FreeRedis.sln index 1db522aa..1dfdac84 100644 --- a/FreeRedis.sln +++ b/FreeRedis.sln @@ -47,6 +47,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FreeRedis.DistributedCache" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "console_net8_cluster_client_side_caching", "examples\console_net8_cluster_client_side_caching\console_net8_cluster_client_side_caching.csproj", "{1C4387AA-C4BD-4388-97D1-3A769439705D}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "nuget", "nuget", "{1C5DEACA-4A4F-4C8F-999F-466B3B95CD18}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "FreeRedis.Abp.Localization", "nuget\FreeRedis.Abp.Localization\FreeRedis.Abp.Localization.csproj", "{F7C7EEAA-DA3E-45CE-BBF6-2978179914E9}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "abpapi_net8", "examples\abpapi_net8\abpapi_net8.csproj", "{E6737319-1109-4645-94CD-43ED445CD7C8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "abpapi_net8_server", "examples\abpapi_net8_server\abpapi_net8_server.csproj", "{6E98AC71-39C8-4EFB-ABB6-4B98C534BBD0}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "abpapi_net8_benchmark", "examples\abpapi_net8_benchmark\abpapi_net8_benchmark.csproj", "{C9B345C2-E5AE-48C5-8055-8ED2EFF2C5D2}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -225,6 +235,54 @@ Global {1C4387AA-C4BD-4388-97D1-3A769439705D}.Release|x64.Build.0 = Release|Any CPU {1C4387AA-C4BD-4388-97D1-3A769439705D}.Release|x86.ActiveCfg = Release|Any CPU {1C4387AA-C4BD-4388-97D1-3A769439705D}.Release|x86.Build.0 = Release|Any CPU + {F7C7EEAA-DA3E-45CE-BBF6-2978179914E9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F7C7EEAA-DA3E-45CE-BBF6-2978179914E9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F7C7EEAA-DA3E-45CE-BBF6-2978179914E9}.Debug|x64.ActiveCfg = Debug|Any CPU + {F7C7EEAA-DA3E-45CE-BBF6-2978179914E9}.Debug|x64.Build.0 = Debug|Any CPU + {F7C7EEAA-DA3E-45CE-BBF6-2978179914E9}.Debug|x86.ActiveCfg = Debug|Any CPU + {F7C7EEAA-DA3E-45CE-BBF6-2978179914E9}.Debug|x86.Build.0 = Debug|Any CPU + {F7C7EEAA-DA3E-45CE-BBF6-2978179914E9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F7C7EEAA-DA3E-45CE-BBF6-2978179914E9}.Release|Any CPU.Build.0 = Release|Any CPU + {F7C7EEAA-DA3E-45CE-BBF6-2978179914E9}.Release|x64.ActiveCfg = Release|Any CPU + {F7C7EEAA-DA3E-45CE-BBF6-2978179914E9}.Release|x64.Build.0 = Release|Any CPU + {F7C7EEAA-DA3E-45CE-BBF6-2978179914E9}.Release|x86.ActiveCfg = Release|Any CPU + {F7C7EEAA-DA3E-45CE-BBF6-2978179914E9}.Release|x86.Build.0 = Release|Any CPU + {E6737319-1109-4645-94CD-43ED445CD7C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E6737319-1109-4645-94CD-43ED445CD7C8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E6737319-1109-4645-94CD-43ED445CD7C8}.Debug|x64.ActiveCfg = Debug|Any CPU + {E6737319-1109-4645-94CD-43ED445CD7C8}.Debug|x64.Build.0 = Debug|Any CPU + {E6737319-1109-4645-94CD-43ED445CD7C8}.Debug|x86.ActiveCfg = Debug|Any CPU + {E6737319-1109-4645-94CD-43ED445CD7C8}.Debug|x86.Build.0 = Debug|Any CPU + {E6737319-1109-4645-94CD-43ED445CD7C8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E6737319-1109-4645-94CD-43ED445CD7C8}.Release|Any CPU.Build.0 = Release|Any CPU + {E6737319-1109-4645-94CD-43ED445CD7C8}.Release|x64.ActiveCfg = Release|Any CPU + {E6737319-1109-4645-94CD-43ED445CD7C8}.Release|x64.Build.0 = Release|Any CPU + {E6737319-1109-4645-94CD-43ED445CD7C8}.Release|x86.ActiveCfg = Release|Any CPU + {E6737319-1109-4645-94CD-43ED445CD7C8}.Release|x86.Build.0 = Release|Any CPU + {6E98AC71-39C8-4EFB-ABB6-4B98C534BBD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6E98AC71-39C8-4EFB-ABB6-4B98C534BBD0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6E98AC71-39C8-4EFB-ABB6-4B98C534BBD0}.Debug|x64.ActiveCfg = Debug|Any CPU + {6E98AC71-39C8-4EFB-ABB6-4B98C534BBD0}.Debug|x64.Build.0 = Debug|Any CPU + {6E98AC71-39C8-4EFB-ABB6-4B98C534BBD0}.Debug|x86.ActiveCfg = Debug|Any CPU + {6E98AC71-39C8-4EFB-ABB6-4B98C534BBD0}.Debug|x86.Build.0 = Debug|Any CPU + {6E98AC71-39C8-4EFB-ABB6-4B98C534BBD0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6E98AC71-39C8-4EFB-ABB6-4B98C534BBD0}.Release|Any CPU.Build.0 = Release|Any CPU + {6E98AC71-39C8-4EFB-ABB6-4B98C534BBD0}.Release|x64.ActiveCfg = Release|Any CPU + {6E98AC71-39C8-4EFB-ABB6-4B98C534BBD0}.Release|x64.Build.0 = Release|Any CPU + {6E98AC71-39C8-4EFB-ABB6-4B98C534BBD0}.Release|x86.ActiveCfg = Release|Any CPU + {6E98AC71-39C8-4EFB-ABB6-4B98C534BBD0}.Release|x86.Build.0 = Release|Any CPU + {C9B345C2-E5AE-48C5-8055-8ED2EFF2C5D2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C9B345C2-E5AE-48C5-8055-8ED2EFF2C5D2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C9B345C2-E5AE-48C5-8055-8ED2EFF2C5D2}.Debug|x64.ActiveCfg = Debug|Any CPU + {C9B345C2-E5AE-48C5-8055-8ED2EFF2C5D2}.Debug|x64.Build.0 = Debug|Any CPU + {C9B345C2-E5AE-48C5-8055-8ED2EFF2C5D2}.Debug|x86.ActiveCfg = Debug|Any CPU + {C9B345C2-E5AE-48C5-8055-8ED2EFF2C5D2}.Debug|x86.Build.0 = Debug|Any CPU + {C9B345C2-E5AE-48C5-8055-8ED2EFF2C5D2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C9B345C2-E5AE-48C5-8055-8ED2EFF2C5D2}.Release|Any CPU.Build.0 = Release|Any CPU + {C9B345C2-E5AE-48C5-8055-8ED2EFF2C5D2}.Release|x64.ActiveCfg = Release|Any CPU + {C9B345C2-E5AE-48C5-8055-8ED2EFF2C5D2}.Release|x64.Build.0 = Release|Any CPU + {C9B345C2-E5AE-48C5-8055-8ED2EFF2C5D2}.Release|x86.ActiveCfg = Release|Any CPU + {C9B345C2-E5AE-48C5-8055-8ED2EFF2C5D2}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -246,6 +304,10 @@ Global {7AC68251-83D4-423F-849C-04BD89F4BDFB} = {DD1426BF-F56F-49A8-9D4E-2BA0E3BEFC72} {B0B0B55E-4D41-4419-83FE-309F72699A3A} = {D8045351-59E4-45B9-81F5-D21CC0996344} {1C4387AA-C4BD-4388-97D1-3A769439705D} = {DD1426BF-F56F-49A8-9D4E-2BA0E3BEFC72} + {F7C7EEAA-DA3E-45CE-BBF6-2978179914E9} = {1C5DEACA-4A4F-4C8F-999F-466B3B95CD18} + {E6737319-1109-4645-94CD-43ED445CD7C8} = {DD1426BF-F56F-49A8-9D4E-2BA0E3BEFC72} + {6E98AC71-39C8-4EFB-ABB6-4B98C534BBD0} = {DD1426BF-F56F-49A8-9D4E-2BA0E3BEFC72} + {C9B345C2-E5AE-48C5-8055-8ED2EFF2C5D2} = {DD1426BF-F56F-49A8-9D4E-2BA0E3BEFC72} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B36AD060-F6AE-49C7-A310-B58B8467CE69} diff --git a/examples/abpapi_net8/ApiModule.cs b/examples/abpapi_net8/ApiModule.cs new file mode 100644 index 00000000..ad75e61c --- /dev/null +++ b/examples/abpapi_net8/ApiModule.cs @@ -0,0 +1,42 @@ +using FreeRedis; +using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.Localization; +using Volo.Abp.Modularity; +using Volo.Abp.VirtualFileSystem; + +namespace abpapi_net8 +{ + [DependsOn(typeof(AbpLocalizationModule))] + public class ApiModule : AbpModule + { + public override void ConfigureServices(ServiceConfigurationContext context) + { + // 将 RedisClient 注册为单例 + RedisClient redisClient = new RedisClient("127.0.0.1:6379,defaultDatabase=13"); + LanguageRedisOptions redisOptions = new LanguageRedisOptions + { + KeyPrefix = "language", + Capacity = 20, + CheckExpired = TimeSpan.FromHours(1), + CheckNewLanguageExpired = TimeSpan.FromMinutes(1), + }; + context.Services.AddSingleton(redisClient); + context.Services.AddSingleton(redisOptions); + + + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + + Configure(options => + { + options.Resources + .Add("en") + // 注入 Redis 多语言 + .AddFreeRedis(redisClient, redisOptions) + .AddVirtualJson("/Localization/Resources/Test"); + }); + } + } +} diff --git a/examples/abpapi_net8/Program.cs b/examples/abpapi_net8/Program.cs new file mode 100644 index 00000000..012ef3cf --- /dev/null +++ b/examples/abpapi_net8/Program.cs @@ -0,0 +1,49 @@ +using FreeRedis; +using Microsoft.Extensions.Localization; +using Volo.Abp; +using Volo.Abp.Localization; + +namespace abpapi_net8 +{ + internal class Program + { + static async Task Main(string[] args) + { + RedisClient redisClient = new RedisClient("127.0.0.1:6379,defaultDatabase=13"); + + await redisClient.HMSetAsync("language:en", new Dictionary + { + { "Hello", "Hello" }, + { "World", "World"}, + }); + await redisClient.HMSetAsync("language:zh-CN", new Dictionary + { + { "Hello", "你好" }, + { "World", "世界" }, + }); + await redisClient.HMSetAsync("language:zh", new Dictionary + { + { "Hello", "你好" }, + { "World", "世界" }, + }); + + var provider = AbpApplicationFactory.Create(); + provider.Initialize(); + var localizer = provider.ServiceProvider.GetRequiredService>(); + using (CultureHelper.Use("en")) + { + if (localizer["Hello"].Value != "Hello") + { + throw new Exception(); + } + } + using (CultureHelper.Use("zh")) + { + if (localizer["Hello"].Value != "你好") + { + throw new Exception(); + } + } + } + } +} diff --git a/examples/abpapi_net8/Properties/launchSettings.json b/examples/abpapi_net8/Properties/launchSettings.json new file mode 100644 index 00000000..8113026e --- /dev/null +++ b/examples/abpapi_net8/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "abpapi_net8": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:51294;http://localhost:51295" + } + } +} \ No newline at end of file diff --git a/examples/abpapi_net8/TestResource.cs b/examples/abpapi_net8/TestResource.cs new file mode 100644 index 00000000..1d285329 --- /dev/null +++ b/examples/abpapi_net8/TestResource.cs @@ -0,0 +1,6 @@ +namespace abpapi_net8 +{ + public class TestResource + { + } +} diff --git a/examples/abpapi_net8/abpapi_net8.csproj b/examples/abpapi_net8/abpapi_net8.csproj new file mode 100644 index 00000000..e09fee0c --- /dev/null +++ b/examples/abpapi_net8/abpapi_net8.csproj @@ -0,0 +1,18 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + diff --git a/examples/abpapi_net8_benchmark/Ben.cs b/examples/abpapi_net8_benchmark/Ben.cs new file mode 100644 index 00000000..146a5174 --- /dev/null +++ b/examples/abpapi_net8_benchmark/Ben.cs @@ -0,0 +1,48 @@ +using abpapi_net8; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using Volo.Abp; +using Volo.Abp.Localization; + +namespace abpapi_net8_benchmark +{ + [SimpleJob(RuntimeMoniker.Net70)] + [SimpleJob(RuntimeMoniker.Net80)] + //[SimpleJob(RuntimeMoniker.NativeAot70)] + // GC 分析 + [MemoryDiagnoser] + // 线程体积 + [ThreadingDiagnoser] + public partial class Ben + { + private IStringLocalizer _stringLocalizer; + + [GlobalSetup] + public void Setup() + { + var application = AbpApplicationFactory.Create(); + + application.Initialize(); + + _stringLocalizer = application.ServiceProvider.GetRequiredService>(); + + var language = "zh"; + using (CultureHelper.Use(language)) + { + var value = _stringLocalizer["Hello"]; + } + } + + + [Benchmark] + public void GetValue() + { + using (CultureHelper.Use("zh")) + { + var value = _stringLocalizer["Hello"]; + } + } + } +} diff --git a/examples/abpapi_net8_benchmark/Program.cs b/examples/abpapi_net8_benchmark/Program.cs new file mode 100644 index 00000000..5e442433 --- /dev/null +++ b/examples/abpapi_net8_benchmark/Program.cs @@ -0,0 +1,14 @@ +using BenchmarkDotNet.Running; + +namespace abpapi_net8_benchmark +{ + + internal class Program + { + static void Main(string[] args) + { + var summary = BenchmarkRunner.Run(); + Console.ReadLine(); + } + } +} diff --git a/examples/abpapi_net8_benchmark/Properties/PublishProfiles/FolderProfile.pubxml b/examples/abpapi_net8_benchmark/Properties/PublishProfiles/FolderProfile.pubxml new file mode 100644 index 00000000..df5633ac --- /dev/null +++ b/examples/abpapi_net8_benchmark/Properties/PublishProfiles/FolderProfile.pubxml @@ -0,0 +1,13 @@ + + + + + Release + Any CPU + bin\Release\net8.0\publish\ + FileSystem + <_TargetId>Folder + + \ No newline at end of file diff --git a/examples/abpapi_net8_benchmark/abpapi_net8_benchmark.csproj b/examples/abpapi_net8_benchmark/abpapi_net8_benchmark.csproj new file mode 100644 index 00000000..cb2e429f --- /dev/null +++ b/examples/abpapi_net8_benchmark/abpapi_net8_benchmark.csproj @@ -0,0 +1,20 @@ + + + + Exe + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/examples/abpapi_net8_server/AppModule.cs b/examples/abpapi_net8_server/AppModule.cs new file mode 100644 index 00000000..94739724 --- /dev/null +++ b/examples/abpapi_net8_server/AppModule.cs @@ -0,0 +1,80 @@ + +using Volo.Abp.AspNetCore.Mvc; +using Volo.Abp.Modularity; +using Volo.Abp; +using FreeRedis; +using Volo.Abp.Localization; +using Volo.Abp.VirtualFileSystem; + +namespace abpapi_net8_server +{ + [DependsOn(typeof(AbpAspNetCoreMvcModule), typeof(AbpLocalizationModule))] + public class AppModule : AbpModule + { + public override void ConfigureServices(ServiceConfigurationContext context) + { + // 将 RedisClient 注册为单例 + RedisClient redisClient = new RedisClient("127.0.0.1:6379,defaultDatabase=13"); + + redisClient.HMSet("language:en", new Dictionary + { + { "Hello", "Hello" }, + { "World", "World"}, + }); + redisClient.HMSet("language:zh-CN", new Dictionary + { + { "Hello", "你好" }, + { "World", "世界" }, + }); + redisClient.HMSet("language:zh", new Dictionary + { + { "Hello", "你好" }, + { "World", "世界" }, + }); + + LanguageRedisOptions redisOptions = new LanguageRedisOptions + { + KeyPrefix = "language", + Capacity = 20, + CheckExpired = TimeSpan.FromHours(1), + CheckNewLanguageExpired = TimeSpan.FromMinutes(1), + }; + context.Services.AddSingleton(redisClient); + context.Services.AddSingleton(redisOptions); + + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + + Configure(options => + { + options.Resources + .Add("en") + // 注入 Redis 多语言 + .AddFreeRedis(redisClient, redisOptions); + }); + + context.Services.AddSwaggerGen(); + } + public override void OnApplicationInitialization(ApplicationInitializationContext context) + { + var app = context.GetApplicationBuilder(); + var env = context.GetEnvironment(); + + // Configure the HTTP request pipeline. + if (env.IsDevelopment()) + { + app.UseSwagger(); + app.UseSwaggerUI(); + } + + app.UseAuthorization(); + + app.UseHttpsRedirection(); + app.UseStaticFiles(); + app.UseRouting(); + app.UseConfiguredEndpoints(); + } + } +} diff --git a/examples/abpapi_net8_server/Controllers/IndexController.cs b/examples/abpapi_net8_server/Controllers/IndexController.cs new file mode 100644 index 00000000..9bd0e492 --- /dev/null +++ b/examples/abpapi_net8_server/Controllers/IndexController.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Localization; + +namespace abpapi_net8_server.Controllers +{ + [ApiController] + [Route("[controller]")] + public class IndexController : ControllerBase + { + private readonly IStringLocalizer _localizer; + + public IndexController(IStringLocalizer localizer) + { + _localizer = localizer; + } + + /// + /// ȡֵַ + /// + /// + /// + [HttpGet("get")] + public string GetAsync(string name) + { + return _localizer[name]; + } + } +} diff --git a/examples/abpapi_net8_server/Program.cs b/examples/abpapi_net8_server/Program.cs new file mode 100644 index 00000000..13741355 --- /dev/null +++ b/examples/abpapi_net8_server/Program.cs @@ -0,0 +1,17 @@ +namespace abpapi_net8_server +{ + public class Program + { + public static async Task Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + await builder.AddApplicationAsync(); + + var app = builder.Build(); + + await app.InitializeApplicationAsync(); + await app.RunAsync(); + } + } +} diff --git a/examples/abpapi_net8_server/Properties/launchSettings.json b/examples/abpapi_net8_server/Properties/launchSettings.json new file mode 100644 index 00000000..f2ee3106 --- /dev/null +++ b/examples/abpapi_net8_server/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:3995", + "sslPort": 0 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5078", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/examples/abpapi_net8_server/TestResource.cs b/examples/abpapi_net8_server/TestResource.cs new file mode 100644 index 00000000..3325ac5c --- /dev/null +++ b/examples/abpapi_net8_server/TestResource.cs @@ -0,0 +1,6 @@ +namespace abpapi_net8_server +{ + public class TestResource + { + } +} diff --git a/examples/abpapi_net8_server/abpapi_net8_server.csproj b/examples/abpapi_net8_server/abpapi_net8_server.csproj new file mode 100644 index 00000000..a99e7a75 --- /dev/null +++ b/examples/abpapi_net8_server/abpapi_net8_server.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + true + + + + + + + + + + + + diff --git a/examples/abpapi_net8_server/abpapi_net8_server.http b/examples/abpapi_net8_server/abpapi_net8_server.http new file mode 100644 index 00000000..59f2a8e2 --- /dev/null +++ b/examples/abpapi_net8_server/abpapi_net8_server.http @@ -0,0 +1,6 @@ +@abpapi_net8_server_HostAddress = http://localhost:5078 + +GET {{abpapi_net8_server_HostAddress}}/weatherforecast/ +Accept: application/json + +### diff --git a/examples/abpapi_net8_server/appsettings.Development.json b/examples/abpapi_net8_server/appsettings.Development.json new file mode 100644 index 00000000..0c208ae9 --- /dev/null +++ b/examples/abpapi_net8_server/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/examples/abpapi_net8_server/appsettings.json b/examples/abpapi_net8_server/appsettings.json new file mode 100644 index 00000000..10f68b8c --- /dev/null +++ b/examples/abpapi_net8_server/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/nuget/FreeRedis.Abp.Localization/Extensions.cs b/nuget/FreeRedis.Abp.Localization/Extensions.cs new file mode 100644 index 00000000..261d35a8 --- /dev/null +++ b/nuget/FreeRedis.Abp.Localization/Extensions.cs @@ -0,0 +1,46 @@ +using FreeRedis; +using FreeRedis.Abp.Localization; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Volo.Abp.Localization +{ + /// + /// 扩展 + /// + public static class Extensions + { + // 防止每个程序集都缓存 + private static readonly Dictionary Factories = new Dictionary(); + + /// + /// 从 Redis 中添加多语言资源 + /// + /// + /// + /// RedisClient + /// 配置 + /// + public static TLocalizationResource AddFreeRedis( + [NotNull] this TLocalizationResource localizationResource, + [NotNull] RedisClient redisClient, + [NotNull] LanguageRedisOptions options) + where TLocalizationResource : LocalizationResourceBase + { + FreeRedisLocalizationResourceFactory factory; + + if(!Factories.TryGetValue(redisClient,out factory)) + { + factory = new FreeRedisLocalizationResourceFactory(redisClient, options); + Factories.Add(redisClient, factory); + } + + FreeRedisLocalizationResource resource = new FreeRedisLocalizationResource(factory); + localizationResource.Contributors.Add(resource); + return localizationResource; + } + } +} diff --git a/nuget/FreeRedis.Abp.Localization/FreeRedis.Abp.Localization.csproj b/nuget/FreeRedis.Abp.Localization/FreeRedis.Abp.Localization.csproj new file mode 100644 index 00000000..231f0066 --- /dev/null +++ b/nuget/FreeRedis.Abp.Localization/FreeRedis.Abp.Localization.csproj @@ -0,0 +1,17 @@ + + + + Library + netstandard2.0;netstandard2.1;net5.0;net6.0;net7.0;net8.0; + true + + + + + + + + + + + diff --git a/nuget/FreeRedis.Abp.Localization/FreeRedisLocalizationResourceBase.cs b/nuget/FreeRedis.Abp.Localization/FreeRedisLocalizationResourceBase.cs new file mode 100644 index 00000000..19163ac1 --- /dev/null +++ b/nuget/FreeRedis.Abp.Localization/FreeRedisLocalizationResourceBase.cs @@ -0,0 +1,41 @@ +using Microsoft.Extensions.Localization; +using System.Collections.Generic; +using System.Threading.Tasks; +using Volo.Abp.Localization; + +namespace FreeRedis.Abp.Localization +{ + + /// + public class FreeRedisLocalizationResource : ILocalizationResourceContributor + { + private readonly FreeRedisLocalizationResourceFactory _factory; + + /// + public FreeRedisLocalizationResource(FreeRedisLocalizationResourceFactory factory) + { + _factory = factory; + } + + /// + public bool IsDynamic => true; + + /// + public void Fill(string cultureName, Dictionary dictionary) => _factory.Fill(cultureName, dictionary); + + /// + public Task FillAsync(string cultureName, Dictionary dictionary) + => _factory.FillAsync(cultureName, dictionary); + + /// + public LocalizedString GetOrNull(string cultureName, string name) => _factory.GetOrNull(cultureName, name); + + /// + public Task> GetSupportedCulturesAsync() + => _factory.GetSupportedCulturesAsync(); + + /// + public void Initialize(LocalizationResourceInitializationContext context) + => _factory.Initialize(context); + } +} diff --git a/nuget/FreeRedis.Abp.Localization/FreeRedisLocalizationResourceFactory.cs b/nuget/FreeRedis.Abp.Localization/FreeRedisLocalizationResourceFactory.cs new file mode 100644 index 00000000..0ac5835e --- /dev/null +++ b/nuget/FreeRedis.Abp.Localization/FreeRedisLocalizationResourceFactory.cs @@ -0,0 +1,88 @@ +using Microsoft.Extensions.Localization; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Volo.Abp.Localization; + +namespace FreeRedis.Abp.Localization +{ + /// + public class FreeRedisLocalizationResourceFactory : ILocalizationResourceContributor + { + private readonly RedisClient _redisClient; + private readonly LanguageRedisOptions _options; + + private volatile bool _isInit = false; + + /// + public FreeRedisLocalizationResourceFactory(RedisClient redisClient, LanguageRedisOptions options) + { + _redisClient = redisClient; + _options = options; + + redisClient.UseClientSideCaching(new ClientSideCachingOptions + { + Capacity = _options.Capacity, + KeyFilter = key => key.StartsWith(_options.KeyPrefix), + CheckExpired = (key, dt) => DateTime.Now.Subtract(dt) > options.CheckExpired + }); + } + + /// + public bool IsDynamic => true; + + // 动态语言集,此方法不会被调用 + /// + public void Fill(string cultureName, Dictionary dictionary) + { + } + + // 动态语言集,此方法不会被调用 + /// + public Task FillAsync(string cultureName, Dictionary dictionary) + { + return Task.CompletedTask; + } + + /// + public LocalizedString GetOrNull(string cultureName, string name) + { + if (string.IsNullOrEmpty(cultureName)) cultureName = _options.DefaultLanguage; + // ex: language:zh-CN + var key = _options.KeyPrefix + ":" + cultureName; + var value = _redisClient.HGet(key, name); + if (string.IsNullOrEmpty(value)) + { + return new LocalizedString(name, name, resourceNotFound: true); + } + return new LocalizedString(name, value); + } + + /// + public async Task> GetSupportedCulturesAsync() + { + List langs = new List(); + var keys = await _redisClient.KeysAsync(_options.KeyPrefix + "*"); + foreach (var item in keys) + { + langs.Add(item.Split(":").LastOrDefault()); + } + return langs; + } + + /// + public void Initialize(LocalizationResourceInitializationContext context) + { + if (_isInit) return; + _isInit = true; + var keys = _redisClient.Keys(_options.KeyPrefix + "*"); + + // 第一次先将缓存拉取到本地 + foreach (var key in keys) + { + _redisClient.HGetAll(key); + } + } + } +} diff --git a/nuget/FreeRedis.Abp.Localization/LanguageOptions.cs b/nuget/FreeRedis.Abp.Localization/LanguageOptions.cs new file mode 100644 index 00000000..04e04e01 --- /dev/null +++ b/nuget/FreeRedis.Abp.Localization/LanguageOptions.cs @@ -0,0 +1,42 @@ +using JetBrains.Annotations; +using System; +using System.ComponentModel.DataAnnotations; + +namespace Volo.Abp.Localization +{ + /// + /// Redis 多语言配置 + /// + public class LanguageRedisOptions + { + /// + /// 默认语言 + /// + [NotNull] + [Required] + public string DefaultLanguage { get; set; } = "zh"; + + /// + /// Key 前缀,如 language。 + /// 框架将会拼接成 language:zh-cn 类似的 key。 + /// + [NotNull] + [Required] + public string KeyPrefix { get; set; } + + /// + /// 缓存的 Key 数量 + /// + public int Capacity { get; set; } + + /// + /// 每个 Key 一直未被使用,则此时间后过期 + /// + public TimeSpan CheckExpired { get; set; } = TimeSpan.FromHours(1); + + /// + /// 如果在本地没有搜索到 + /// + public TimeSpan CheckNewLanguageExpired { get; set; } = TimeSpan.FromMinutes(1); + } +} diff --git a/nuget/FreeRedis.Abp.Localization/README.md b/nuget/FreeRedis.Abp.Localization/README.md new file mode 100644 index 00000000..963ed1d2 --- /dev/null +++ b/nuget/FreeRedis.Abp.Localization/README.md @@ -0,0 +1,78 @@ +往 Redis 填充多语言数据,使用 `{前缀}:{语言名称}` 的形式存储 Key,每个 Key 均是 Hash 类型。 + +![image-20231209085550710](images/image-20231209085550710.png) + +使用编程填充数据如下所示: +```csharp + RedisClient redisClient = new RedisClient("127.0.0.1:6379,defaultDatabase=13"); + + await redisClient.HMSetAsync("language:en", new Dictionary + { + { "Hello", "Hello" }, + { "World", "World"}, + }); + await redisClient.HMSetAsync("language:zh-CN", new Dictionary + { + { "Hello", "你好" }, + { "World", "世界" }, + }); + await redisClient.HMSetAsync("language:zh", new Dictionary + { + { "Hello", "你好" }, + { "World", "世界" }, + }); +``` + + + +![image-20231209085649631](images/image-20231209085649631.png) + +![image-20231209085657772](images/image-20231209085657772.png) + + + + + +在 ABP 中初始化 RedisClient 和多语言配置: + +```csharp + // 将 RedisClient 注册为单例 + RedisClient cli = new RedisClient("127.0.0.1:6379,defaultDatabase=13"); + context.Services.AddSingleton(cli); + context.Services.AddSingleton(s => + { + return new LanguageRedisOptions + { + // Key 前缀 + KeyPrefix = "language", + // 缓存的 Key 数量 + Capacity = 20, + // 多长时间不使用缓存将被从本地中清除 + CheckExpired = TimeSpan.FromHours(1) + }; + }); +``` + + + +然后在按照 ABP 配置多语言时,使用 Redis 即可。 + +```csharp + // 从容器中取出单例,避免每个模块都 new 一个新的 RedisClient + var redisClient = context.Services.GetRequiredService(); + var languageRedisOptions = context.Services.GetRequiredService(); + + Configure(options => + { + options.Resources + .Add("en") + // 注入 Redis 多语言 + .AddFreeRedis(redisClient, languageRedisOptions) + .AddVirtualJson("/Localization/Resources/Test"); + }); +``` + + + +因为 ABP 多语言需要每个程序集的模块都初始化一次,因此为了避免每个程序集都做本地缓存,我们需要确保配置多语言资源提供器时,使用同一个 RedisClient 对象。 + diff --git a/nuget/FreeRedis.Abp.Localization/images/image-20231209085550710.png b/nuget/FreeRedis.Abp.Localization/images/image-20231209085550710.png new file mode 100644 index 0000000000000000000000000000000000000000..33ae3f5edcdf3f72c95964a8cc81793f3d4fea7e GIT binary patch literal 6989 zcma)>1yEdFm#z;GAUHvS1P{S2NO1X(AffT#7Th6?I|K`m;I6?P(zr`-cc&qQ;M%wZ znDhPL%)NE1{<$+%U8i>+-M!bl-nI90!c~-HaWKg-0RX^}my=cl03>v<-S!k2+`c~M z)B=DPMDo&qX?SED!acRsEt`Q8evFt9db2>e&sf1Sa-AW-5=(t%X{Po2j4>F0H4ie+^X7Q~4cJSe^UpSe`;; zNr|7rK4{SNgsKMOzj%u(Y4_jmcrN>}i*c!D=+6%iYLMy04C{V|aRW_F$;KDo!L&cZ z^(UdkTqmL8(dUrOU{X|N_zKGq0Cq4g>xEVnLNpTDHlTvAVyaInOb7e9dXx@dbp^S& zmR2a&ojfYv_HKM|rsTQ5r{$kr+&UsPHFiP@IK558w2qSwrhO)*&%r29r^b?>FfV>$ z;7?MK@a{FeNo4;9%chz?iD)0R5tx8n$q|IWiiu6)=`mY&vjT^ua=1c!Jboe?O^*|x zNHi3M)Nzsf1QjL#$Sb+L;Aa^)y)d^Oo~!s!zhr=T4%l({V*^Y>cT>a5 za4I~&4z6Zs#ze7PBw=T{zsFst3llJ%a38!HkKDNb-DDXTp9mPkw2jsbV6M(hn!?u% z{L({g>;{_k4qw-f*I>@clC*25JsTfC<_Y#O%rh-x=BQl%!V<44EiW!DpV)Whd|0kx z8hW?N!pvM+TE5U^ddHkMXaYkHnAEsC$=i%gpGLY}w9E#Nc_2cv{cDebnnMjj+^}(e zUA=?Kzt-$wEo1z;MaSV<4a;W;#_Yb+y#uYdL@2Ihv*F(0fE@p3AVXA7 ztNljSa(oV{Gu_|kPWkwOvrI9$?65of5`I%5T+Lk>HnU@wU6|u`#97sr|!i$&&&Vr;EtZ;o=#cMi&!#i>=eF+r28|x;0)Cu%1U1Dm z!<)ZoxS5z#jHH?xAc!t>HN_jOLb54qXm)LnB8c zBeyO6CPkse#O5ezI#1|6;mB~etl033jpKDiVc}S_?L{WFzuJAyzHF% z)b%qi?iV4WlAEjRte>xta&mfH96@>&jhhJ}@oZRhKF$s*F$tLx8Dx@w#)d~TfkRwR zOT0G^8Tid@UGFwl@Z1vIYG8%KwB7dE`S|*jOYvUkIZ#Dk9ZZX}Fb!DFAv~BkqL(L} zZ~^R)$QR^e^ZXg<1!}QpTCqJQ2t2BV+Wpyiv4?!G!?YH3_R&q~{l*#R-rkF}k)*F{ z9qyR8BPLvx3t;MR=4e*w4(j?1S&@^nNc(mRazD*sI%du*p)nG_v{3|_6q~ukEg3?d z0H39E>uir;y7qw*0HDFlj2ZIlXMeVviwhcZV7KhU(czAqLfn{I2`=?Ri$Y>97&zny zM3vHxi>C;cqj8{4>e6hPrk}3w&Yv5sproX@IBtKxFYi;O(k52bxZreqP6ptIOGk^| zo=~s(ZKrPgkyBH5xX2^G_?&u(WO5wa8MGNDGZJBLjm&9R)Q`|U%nMMAl_t7p3Hz^u znRC-**?`0LwR@e-Kn?nkpl{j3Q@J=Tz1hkvcH?kJDGoo0$aGcwVs4Mp)7sF?f zy(S-jT_2jTjb(PI=-iA=FK2-nNqF`H{oX+~p~PR;(Cjb%gft=B>N2iICxL?tFFfiS z#iHT&W$KJALP6bk-zI{qb?rGH?);zZbhfRrzi`p!CN|a!WJu6VM=X_9DY~&c0L!4M zHv*mOPF$>$0vI7b)K7dwBBHPWI@&e9E5m2S{Y0(>Y1dZFq!K7@cxXxT^qyaBgJIS zDXPxDWCn``bN7i;DxSiLoZ9V!`=0#2rA6ZZ)*2^65nid4lj_}*^;Qq7(9dGe)<6IJ zuhu{Was~t7R?vUesu7C-21zC&p|u6GIs-bvm+|>=J_eP# zqB7HG#v0!~&d<*Jo?j0-F=>1of0vm!wMNaYXJ<4^YFVO|L`3p7^`>{b$nm;|F1r8y zPyxY5Hf53z8s8AJ^YiWp*YYLOt+hR!n}&z5?R{sSsckAzzbWmz(J6v%mF4CVr8 zOlv9%3ND76<|x{moh^WY@nH&Y)#5=roUrcDA0diz_~GHXGFaz)eN_hbho!U@GJD5( zGI$uk&$A8FhwD2~(j3X@iLYhj3nQwPwD{ppt8qzb1ip20&{UVjDH*PVjq2GWXp;!m z`64v6cJ^rR+;#MO@H$UG_ikE+{01XEwhyaok^;-KhPtM(h=@V%HgAB@L1VBwfU%Zm#$Y>5#D_EbPhMQUQHTAZO~{6+Qj z<{P#l+qxFEQT7~_l-<1)rR>P)=>47ZL34T4G}OZAo=s@&nT0IXJ!b06u3eR|rG>qN zknh;qEDoR$!Y0niQ*HV-sdmZ0&GH5CB&e-f-`?GwrrH6(#|%*-A4l{0gzP8T&cUi0 zJpUjY)V&@oNn2PWsv#j}GC~&gUd)b+z!NX8shQG|^TmXQGqp$_ z?p0k%d&*3)!`4Oa)cnev!Ch#-%}*D?IE7dC_J?OR=r^U6ljUOH5MN0lBNtKe>z%!TKEYyHMGq;vz=5DDSFy670)rH(KSm1&&2oReoMY)LW5231i+& zG4oqpme}C#^(ZAHlUysrk3XSjUl9mI^Ge2G<%VmYP$d8qNEWK%aSPhz$}tgPrcAog zkbYS)OVL8%p0=EJ8Be{9bh59dT=box%fM&%h_;xxKFd-=Ygmbby@p@5lI zOpGH+w0=6jB&GRvo;{J?Vk()=lFQKYx{KlZsmD7tOZfrVq zHu3zRdL7tRdor&0KN!t^8_A7_%6$9b3U!Xg_C9|RX z@vO@eF@gQe7i@*20Dh9KZ!7X!{Thl0d2Dj6Bg|$fynjg;kNt%p{H6Z>^tBdtPAV|| z7kH!pab5Fo&(Na)X6EOW-T3e$Ubh*F@7cpD{;~0T)vciBsX4!r-brdqBp`ZtR580O zhOxKSU2o zm8A(Q!;kMiYPny@Pp3XS_yH;fOpfe^6bFa*r)Kuml7`me)a5@X&PqPmVxs{HcfVi@~X4Ce+8hYQr33?cxkptv&G$*tEPnWu; zB{*zp!&P;1b=B`@XJ9;|^5wlwND^X1JR=~xyW48U_y z_f$4lE`>hP<+gcX4Rm(a$`qMiS5c3`CgF+TBv3&CXmw;14bLcUha(h1$o?{@=&u4IFLe));_~Yi{4+CD3_;^)+^Q zP|t`;90_PTa|R?zG*+r>gUdU%vK&z)xJdu(huvX^_6jcQ_;ox!K2#@V{X4*`L)tH+yunw`W*9{)&^Ygyo&{yYCI| z6I6F{1|-BnnH&_j*`nS_Pu?mijUhJr@BKRD=yltDJk<#9R%}aI7Qd_bFCLYbh!Q@< z7Mx_t&i>9M?pUnefE#ue-cJoqdcKmRKOmy>P;Z1_+StH0CvT;m_f?vAJKO_SaywzU zd$v_NTVj-13|mag_Bss@4t@eP<7{!YPKdU27jW!oOTfU>J1fXIV5@#vtsd5W(-n5d zJ+<9c7h^p4__N#Btf>2rVX|@O@Nj>BpAZxE_bW0P7nj?({J7<$M6TB~M3}d?w_e_N zCe*}EheCoF*aR==VvB2QHF8wG&ndFv>1`{Uz-^?XJ@ic?%Ze*73t_v}WPR_q~a z^BvGRdsO*w2SaVn@O0({Bs6#Sj-td=FPyFfoSqSqf|Q^*1|Rb}G{mdsaEe&V`kEonM8wmlqH)9zC~#i54W<^-Wsj(@Jwb|E!IBO!BaU0Xa6Bub7dN(p+aE+(Eev zj@YyHH%?mN+M6&_eZ@+$0|krup2mB{r7*dm?9^9*AKs)Va~g?p(F4Y+ft7G69sA@w zOWUZ(NIi{j@d}|LQIxor7wh9x56RS@drhXS!wuoa$`jjH@u|QdqCx{#O|-Ednap^l z!x{yk__GYo!t*Dux7PsRn+6MXdpiaRElmgvN9goZ={L-#yuW=_yxRN@m+FE-pG-P+ zQACgv=;O-ri#yL5qkIeMf zRu-)%-Lu6UD+kZ{c5AJsX5w=6h)pg^9q%td5#F!d2rA++G76W$V7qvZ?KmJTkc6a> zSANBL_>+8vB*7BP6|z=c+jDpslgMYi_sK^z93Q6L>Ne^wcI`whDB8Py*5U6+7RAb- zCF$c+ExEeqGMb(z|2Z1({M^Uq+WAz|;#Pla$e^eyGCT&h0airoTOXO_lBsQRIG_IUQ2`n6MVIjyP3BX}HK zSSTIK#mECchxlq;(VWeodVAa2jOUlCg+`>RrdaS`@g$!BV8=zA9?=kTO7hQQ=W$@^ zyzC?Cd9;?s=q6*ceG|M#f6)^4LY9Igtul;3koFwibT~9%VkF4^-@&vE0^+yohr^FI zM!&^mg9|C`SpSAk5})l?j&XU@Y>fQ#KK*HHx3dT7%RdC_ImwICvgk%pDv$zgSTvUghYqQD*?&uU7^Q%8LTx zs}>LusHMtM`NbS0Ftb399b4-oPgUNkvwM~m_G#;z(NO2yPJn(&epiJ7d9^`Bd}aS9 zOP>QJK(RuThDc>*7R7hE5XbDspiI+k+Jy!moZiEE+j7rshN2-a_?d`KcS1(^gZHzRaE9wXmHMnZYq3hPZF6232d93%ecVT9X68KdkWby_po~ zi7=mh0-@;QHc|k{3{e6l_#v#n51ev$fz2Enn=kp6G6_i>(~@X4S96+oHR2CsXAW+f z3|Vhm1iw0JXtT{Vta)A)7(J?SB*!8o&M!o%Sn!|5WP3*B%CX^u1o!OCvSvT_DkB+# z;WzUP!1Z8?pg5u@1?yan3XRr4g99^T$1Mu;?^9IN8|4J?KKkF1{1u49zhoZ%q_gvr zGMd@c0VlnaroYWE)?bb&yQb+H7_CVd9(nq6GBZ}|d}FF=8bqFNRtv*5fY-O@1X{~S zL}Ghi9+;!O3GOxVJz^AZ&@Q*BODo)!3XASJJ z(xhK+ezpOQ4P9miDBFc48%D+UYSs%j&nUgg@s6Q9Z&SSTg&f} zMW-o0|1+5u5gF#oJGtK(#5L|pEVx~v10H4$X&QzoQ!vA`|1S&=6%@<;nJLd0<0Sn|bNBZh(++VmxVL0h#sLnKWW>NBue4T85rPe)F{C-O-+TjA3m zwyv4DiQ-O*9j(A`Zb7kNNhFLQP&Bl3{r$5;`pe9!W=!R!%p3F}D!NQiEQR~3wWFbNpp&iZ2{k|M2;w6g@3H+J9SLZW=NANj z8rUn7LB>UE?DMG!N4fs#c*JQt>~0i%B%(_+SWv8T>FmLiL6nN-|Oh9Vb{TuUA`|m#(5J_Z*IV-3!@O2!RfjBWwD_?DeTWf zBrE54LvIN*tJ=>p-5<6=Fr_}l zQnG6P{d-M~_nN$^jwc6H`osS^*SX&6&prv zcJC2SL7mU^m4ThRUlu$#)A29Q68cUK+D(%rl{x+^qNn`uhV{t2el$1*gfC^o^b^#_ zs*a3RAz5pK?P?ln>zWi2g;%I7<7Gk)A@?j7xfN_6E*J)N-!iEod@vryfWCFzSXtja zf`B#sfeoCMLKRja)A^g>)rtBX%eB_sS)$%&XBYe{sAswy>#XzLd(Y*cH<_8dvuAdhJ=KybsK zfZ#9oTi1Y=yPv-15D+{hPzK5A`R8Hh1A_GqJFa#|T(a3`)$Y9zd&tnwcULPJD*w%x z!lgrJ8xDudQZ&yjEEt%;urR+$GZz>Y&kym1H}TIWtpCyO5Lp=UjThK$K8Wny1cfMg;ok&uQ-X1p z#nr%E%XwoTP@4w>)VfZw0fmW7aruI+ua@Hlm0`$rofJg4|8=6P-%$nOL}eBBPw%wR zc9?g0SdBxQk0dX)1{4=u(UX+7NtcC>SBsxNocQ6QuBoc5sygK$(ru_-{502C^^O;% z8L%kYE7OA7VLn(i$%kr51t}Zvx?bkv!iCRYWG@~CpLkr;$%sBU_!~JhS?o7^sJ*6) z&O)pGE8UC(uMNw#Hg0L&9&5pbB3Tz;&$Rn93akYgq}qb72j0&YjK4P}*YP%p>_I%? zwQjv}&RD{0(nRFF%mK|{!-vm4GF#m}qiPJsy+OM$ZUd%4z;|=%AWex)s4!}ju6s=| zTHM3Bx1P`RvqvJB3m92E_Lvz04Ew--m+_Ts;xMg3`rFR8{OVhhw}96Fwq|;DMmN^h zE|A1mlcUpFYcIZGJY_E~!|G?9@0s<#Ntho5)bCL_mXVhAAawd-tz<=!SycMsV^b;H z7!~Q8P&PPS{_7E=nYzYd&?BG^|k~js{iZ)F**!iXM$WIv< z^BCTy1>OgQ;3zqr)f8;l?oF;MsE!KJCqS+)BqP z$gAvHx>NV6m@hMWjfUUg{U#0n)U?ae;Zghfc0UnsxR|5US-bei*0kwqlI)h~IuC@q zbu~_x`9k-Q|9#7X%+;Cp{YKx;*6N}nZIF<#klUx9!U`pIfFfogekZ{)9p+(JI5$!U z3Hqg`yYCFcuCK4hm}N}Yr+0-xMmSlH7aAO`(ANOTk4K)hS)Y!};#P@SJdv#oD5x1L z3t#4WRN_S`v_o2$b-8uZKwp1$XQ6S-4Tf%YJvYl0%Rz8JMy@7ScsHkNx%l`byU-`A zImX61Iyx>w*=ZTgZnL<(DcQJwDnp+3Afy5?cA7u5CGfDSS0T#N^ffR|CcnuOmPy~B zn*6S--b*{&$W4OJN%`2)ocP`AWVFRgOU0a_m&Aa4;juP=>UY!pGE?B?n!J*)t2k&$ z5LpJcLY1S;5A@xCj1K1)Pj1*D_O1>pIyAB$_&*HZdgZV>d_#}wc4Ub(af3nYb%d0r z>?V28Rl|n8MaR~ax%m2&HdCXdq+}{@MuOj5ZM++^WU%d*i-0!29ivYkUk--Kdune- z1a53>Ty49fh^~Ec4O`IGHKCEpeLU%V-rRY52uK)?i7E)g^`tCWQ#iW&0eb|&9V2{$ z5bgzY&+LzC=H})&A!pb~(qLHX{blcE8{iQk*e6|!JIAMsL9mO}MBMbw@zw8S2xE1P zo(>}c!CHmZhd$<8<>jxe6&3V3sg_^yICq$sTM4m~@_`ee;vaWA&x^5Gmg0f_TNh`j z+cm#VH^PEC&lUrjo=w*Wnf=W763%E}3@t=eCX*SPLv}RzFAOh|zsHkM0MXH`Gq^IE zHlcT}oe>j>w)Sl?-KAk5IpeN1br7LXP_VJHQ-C$xevPeiPGey2>7k<&!}uyhx^|zL zfY`*v#f4c*<}prwEg;*e@7XNY4MyTFpD-QanCnhQ2_fhE`}?tRaiG#qpOlo1WFTRv z+WBfjn2tgjH7`%lj%5qlC2*~c{9BG^%N)P^<-TE;XD-@h@BEUZ!LgghO)yv8uA#ED z@l)NH+~_p|%jd?kbB77)x*V7lTl>Ma?b7$Wb?dVC#7l@eXcLr`311pEiw`8Zh;Wb= z-(2_{F2K3G!7kx!E`4e0y1IL^%v5wl9cH6fyeey38lKVPVji?i+9B4h<<@n0Bt=D) zK8Qs(*vIw{YQm=LmM3|;f&!1Wvg2T_^@h3z&Sy&cA%eSCidrvho;9Ftr7}BR=$dev zJBel2t8a=-6E*Xp)ZTwI(f-*Kwu z<&iZ_bDK5Ahq^JrXgeki`z`t?WaY(+?{@pgxLv7`mB$nkuR@hTC(nW7#0P`XxSQOH z)PO3a=DC!o#!}p!y?l`^NNWNUjjrio@Go3&m;G2}+5Po)wYBaI7{q(F>Vi}f`V%WG zH3HFjT(TO}IOmoq^n&WvjlB2eC%PfSj~Ln0s$9f!qxEL7SS$@sJzxhN^FB58Iv_y4 z&JcTxN4bcpsfBHCNqJk8bDO5VzQqT$M1#G)zCO3bTW~Wf`1cn^vI7~t;<73 zn1lF$Sks>v8u;-dX$Z3bcWxGnp#V-uZhpV7Y053@>s;PCflHy_=7|PzxuD`Eqzo_tG*si{zueUcx>-Pn~vH>$bUuk}DKe@7uy-w74 zwEx+AQ=89{XA!cKS0>{+BSyObXuk>ji?EhUO9*c{rG^tEy_i zV^P)E?uG?46EG5DVYs~zG{OO9&x(F_^);@dsw&-=%?%|7WM#2=TWa9+A^&m84RniF zrswR%t~|3+O(lCOYw`z;<1wX}0oyz&=Ntp6{wxs=AeUK%XnNnaXY%T}JczogZ}M0^ zrg>H|=1?-G?@`Z%&FI_AL4-5gk1>$9?ATWPlOc*(_ymy2;MAjYKGm4*VW z`uR(M=YEDf@lY1!ErQRl^vpIHwH9sSeR~x;<06s;N5wxLJ6kZU>Czg&reLOAl4d)U zKRreYq>Iej2xkg0hF!7qoz9CXZ)Jk-Dhx$WvG=&a0&Es>K|2c~Yt&>J%vH7Z!kl-3 zWM)~%0aYtkTB#jTDEeZ_kK%rE&<@(W2+PgKhU1P$*zE%`*(VoK6Z4XYbb(;Y>`{%OH*!6M8k#pa|xMJbRF`K)8A7T(G+JhX!9G;y%2J#2X$8!aI zF#eDD=?HV=tuIOYSLVcjDI z!N2A+%%qb?e@hF?5d%XB^m>oB@z4Ly3I2cd_JBOzZ-s1SqOkYwkUI-5C&_W5?hl2l zQkoF~3wc8^0i%Ed-5v1KnlHB^!0yls+pywI{2~S30r|6ea~!|OTzb4oXHvVHrZ*5L z2nZCky53RS7`^<)kGHx{r61`C@y-92x1D+`5@SQnlq;MG8Yh4HeL`lqmM@vfBF&wD zJ>+72WB>^{pH7}#9jfIYinB;MZLv=b4vN`2DX!t83wBk>-5tSIvYf6vwfNq0#b3tW zSks126$dMi$12!ve|ywSl}^%df#9L4YE7!76S6N>8)EFOz>Dh-c5=22t6|L)mMJ{#F;O( z{+Z+2-8ea1XdO>Ji$X`Ke?f6O60Fl|Dy=N^y%?#KtX>XQ5qZMgblB|QVZfpt zU<=cO1Iw%TPjTT%<-TL9+A`N{CK>Lzug0asFbKmN*=K(-9#&)O@b8FZKo=g9XjWuH z=|gopwWQ~X?FQ^SkmS7k*_o^?fObmh!D0FB+T89FgL_|z#m>K5WlL!rP!H6tG$^iM zl$=srvRD~bqNDv^j!$sex0d#EAUUJO*%lsfA?}82NM6@@UEXzg4EmMy64z`%jZrO9 z=U^L}V^kI%MAbeElB+i4U?cYtVgrPFv+MNg-PGQTf%<;uNzaekpkI58M%s_#_T(#) zJ@npLQBdR?q-BRoCV)nm8fih*$x4kob{&t<0V>;%Q;pEfOk215ZQKX1rb4}K?V3zQ zmF?_RRh#{<>7+c8OvrO#u;m+f`c^|j;kc0!c2=F|z@)|s%u2w=)h*=XVO8J&B9j0c z!ndk2j_KJM(<1_OmNU}WT0(PpzNb!>Sqnu+>%mn8cnsB3G^qx#`|O+;YhHKhNBF&( zXz;6^>2XPdBf+uyQ|g(D1~JijPL$yW2*JCM=usIqGjqSUOUD9l6z{svHR@ zy_P2_6}m(o6TMZP8qneyQW<=u>p8>fx}fQB)%AX$QLT;^1i~$UQ2W?F&b9Y_^&3r! zAA>yTeY29G77wAs0?U_=2Ou`aWb$+Rm@~bi;3_S=a&lVvj!H+E(nU&ma#9u{!WcYFqQJcWb?-grW{iVF-X=Eipj=tS-k$Km7kk8u!?$pU}r6pi@DbMOsb z{8NkWA2C|tpSmya7!%HWWC{-N65jyE@o@nSsQYnf0eo1q3pdrxTVuh$Aigm4CX^_1 zF!n_xn6SGrl`3Knm<#piJiOruq@Kmj_+f8eM<4|7AO7vc|3MA4_7qAn-8M2)}G;`iZAsEsfQ ze#pPvxPK$y8Hfh|bJNyqUy_FAF^4z*Y|-1CKN#3^>l^Qi6zomx6pVc{^s6V?S{XmY z;E@9Yw#cFR^z7t}0w-nfM_SCzcqP91_WSHw<-iKa@U0d8oT?V+u~v{e!#>akS|J#;cJg!(nob z!$VRsd*mM2aHK#YQ~1HXGvSs5I=gfflpL+(~$RLe7{xtWjL z7f)vp;!q>6OE`af<6-{s;Q4pE%!itaeFM6a)N!Xs?hLXH_(>hv&5&Iy9TD(e-5@1t zf<~9Dr4XGNU-Oqj{v4rzSCBOP=1=+rg`{(C1xQft?bo+;bjA)o=`gntbMK9&`r+x9 zS5uX1AlYMg`Jv0qY<6_iUjcL=IsbjVXSae7y5tX!{$8LhbWpBP@iSl2L5MD$&FkRO zrmRPPm${Z%a5krV|udU4s^H1+OPdxgE z3z7;7YTK)?zkROiY!VpQI$%4qcfOjYJ-6SNv8?!J*_|7b*x1w*qhw&P644tJ0GI)- z8>hw;2dBxs2lDhx2m}J_Wv>o5LIR(e^5?5=KwrC0qa5fG6ykc8cKvg5O|0{uu?wm9 z54LMHNrdGv8r~8%y&s8qQwOqK1cROx>}#T3rev6xj)|Ifg4IEt zdu607dtX&eEc_ie&vH@Ft^l*(V4W3~Czmh4N>#Fl{?C}g(3dE!5374E4QV6mNF5ze zX-TznlI+i?De39iwgOM{)Whc6Ea*zBs_NtVH-i?m2;n4zA)fW@{NjB-T_op6r$^V~ zHNi?e;3G@@^m)66-WAf`FuO7A;pyeuIPvkhIberh6cr%4&cg;NGKZw+(ytS|F`qGg z02z@DvUA*0a@i{w$kNk!Ryh+Y$vtGC^XefBRUe@3!su7fO7@7G3GY|VBFop2cU-3$ zW0xoG8tO_v7GCu4*1Ey=exrp|Q+Zc_<|3^`)TVHDJb2$uj}hkP>=&nG?4$y-7OHVn zSBC;G3@Yn)VFkOY|FukBH2!SCZ1hQ&cf7xm2955WU!OOJe6d`+@=qS092!DsKM;Dvz5jw$18`t@x(OlL z1HeAE`~w4X_T)nO2UpKn_FCW&edi(gYdgT_XPdf=4Hvbv175_`uWCwcu5DCRyFb+j z9|v|I%QH;Z(mO^cgebCwxH7-oo#bjDnlB)0u3(jxmX-?ox)9?2K*CrM?D?g_kVjvp zbgcsC67hADnBcQO42Xr#Pf-r{?bYuOwXPm*QieQSl*z!BsN2uf*Som+RYZFSg~&i; z`R(5gA=meBg1%wa?2x<*Bwe^q)DFc*37HXrw$3|kO6@rB+0C=YwEN+5unW2-IGmd(ohqKpUN#C4>_sB%PP)-Lo?f0 z@7^+oR?1jBq$P2L8<%h?gJOyI)64g~I3JVWPd+%%1HUL7q3ItOkd%@T@Ox(t+tk;o z?+M0yl>ma3tCJJ7)^+XZEHE&To_=IYS}XLoM;U7Mm?)e@=XHA^F#B<0{+-Ad<}eEo zfaYuXmgfNw>A*+JX(6X%pvUhFm*gg+@cxi&sTBFA`Uo{2DAhEob@K07_W zB1E71{qm$X^Z7#uv9LX`n6t5lM#k)Xc4~g*q?2kKM-LGvm~c6k9Xv>D2Uu{^bdPnXPxoRTq+CTcZgYb{LtJuLmTI>8clK(5q46u=7*^3}3*N3l z9k=g(_Zs21@bv*soa7R!z9*Wr2a&k6(qPZ9Uk*J$%f0441y$!+`IRm+mO%BUc!*e& zHF|P&C!+A+$N9df`fkYUz@Oc&N8i_X7fS>JJikcicFt_(oX|9noW7;W37F+Hw*a6X zWo4_kBQ#Hq^u>ehD$y=3PEI~8QcKO}4P_N#qRlZ%q1!hol$){;cB!-Tj_~rC8b@Pj z0~8_L@=$TW@@S^FKERP>e9dKS^AOmJk_Psvw8@;AnT^4LVYQ|#hJ0RW>7OSmRn81) zfgli_pGs2}+}Ksu%OrU8&W(2}X`XFT;a?VX9bMKd;fG}4$fDM+58A4f2@0~i#bTM_ zNIUJzmUMoe&QTOy;+ttg=U}Vd=cEV32tZ|DGZvR=@U9cCYqrV5buq1XvEQYr3bGsF zfmV7w&ee~OuGdnLVCcFEx)7tCgp)#{eBh|t7?rSxkC_)qUM~5H#$#QQGji^c5H5Eq zR*yvWG3$vnM`L}GZ3P_QIq%wISe6`|vL!lR-45#Lj8|0-RQapKK2DyBUC6ZEt{K!pg-8k6#izwEC}1`wYq3NBfl?fA4u{6|Wqb=VDS^{pKv||BTf9 zH)Qp}Wqa);hG|L?m<2!t*W5=3>y0AeWcaVN;{V~fV)zK&IBgx}Kxdm4;f^K9bv?H% zMEMCZfNwgvc-NS;^lH{3EHG6)_5By~mRS5ZSagYUMihh$-{tA=sis(0L~}CdZm;h7mSFVyuF4?T$2A zt4@%Rfm~H0Os1e_x~8V3i~x-n6dGy{;m2?utt3BY)??)6;}gnN4-0TTt_Q0Qhndkc zFrA(T;2E2+qqRiqK7i5--Fk(o$re^@GzB!n!ZI({QljRT#lf<)mUeEDOy4{Q*YbW@ z5IjYfgk>JIuNdTCq|R0D?0i*oeFrWtZ`{UypMt`1JL9kcEMK$*OA`t1rDMtw;JNF&T(W$i1 zpX=|HZMQF1k32h{i#%)w_>F`FGECaMxlJ=_4(@1U9SeYptCjmo`fc7fF$}Q&E_2qF z%5ay8N>u>gDnSi2QSR+Tu_0NGEhMshzZ15s-T^jF6lrUP>(gLzNyBnt3b+Z4eg-A% zRr~!4;5SabZB+|a)>;PG3?hukk~-W@Y#H>*ptmqfS>F$ zSI67RT<+w`+io^t@tZgwpA=tX2E$TQm_#D%;2~C*h}{Yov1%dS-vEW$K4;p1{^i+% zJ<7q;D!vHm;}El3fX}dLC_WEu)NFmn%%rTG@?!G`!?P(KsCkQXa&$jV^0xf1s&_@y zmBp_Hjnnv9#_5uT=RLhLqWfFdS%ek5#JlfOKt^_){CbZ2GkHx? ztj?!R%;`AMA92)}?LDw6BzU8lFLTz8;p-KH!1K^B%kId-F!W(g^e5A8i0sh`p1Jhg z00`baPcMLAFHxiMZkZ#CTHbEk)-x}hp5pig073U2qaQYpWGWq$uo@?{&pvbwcjBeZ z$b5p?7yf2_*Ifyx!){T)89CPYyRz75Jb~HuZ?ASJ!VUI@)l>4Mv~g`Z8gR-+f!f16 zmMr6|h1B;pi8tMSTjmC_;zwAE#*aKlDXy==h5(K%czDYQAeix7zIfN+5A{tqHp1&b z*+Yjmw~yO^3N-})1ag!?@(aWRgTbADW8LeJ-s^u-ms477bZ30ntwMV_Wd{r3Z37!A zAVUd}U@JksUKg?K@8wf7u3h z$D#-Ps{}T|!`~8dOd<*WS6rz#TlPlX0RMbR#x+D4qiXbID(`W;!%xa!OcTHK5rBN) z!o*^f>=b%sWe$Gyz_h1Mm~nho?N3&@0RF1z|zxdeDoVWUEU)Z z0vp-ShWSURjnF$Sm%Ih4K0=q|UHRP05kl7{31~$C2KIuOKNOHFrr)OP1XEe%#7@gy z61N)0c?dB5gBxveE*!T33gR;|3OpOrsR_;JYaU_t2+#j3U`aYVhYOXV+q z(SD~*r z1uH0?RU_z4z!5?Z?stEj!b6D!%Y?drIJW-~e3&gWc9TFWdlrksW-KnY>{c68i{hH1 zT^8%`WiAJ&9{}P}Y~l5JzX3qbfj|Y)RkNWRTPIO+4430`{XS(B2`UN9PoeXnI&H7z zK21|I1v&>g!8MsCxSDs2Y0Zsj=>l_?)1Bm;hjt1=fqH{cGltxzi|zIL`cvYS5lV`# z#j7C;={l!L6^xs@I<{fta(DB1vE*Qaztk`a$3i5i=H%pb)xMlAwR5u3!{6qNWE$iS zTKi2A)!niZxtTtT!9*z*s{njV!FCc*1rQwK2m&$Y=Y&m{Rr@%LWkmSABs0mE3&5lu z36~{vl5NzKuc4?Tkm}dn*q)59Oy1~bVd0(k+>ifVKQI<@n3Dkp>B!Ci$eDy# zM#phAM07bDDYQuYWqu*Xx6OVyujp4bzBu9heZI;Lsg1l5`eJXdTjgqY-NniKDHB8@XUY~8Wa1Tyl%QGQ9IWb%*IrpiO2QwNfH}Vy**?dM~>}Qyv?iIP4asN0u z51nu)Z?~DLqf))c{(g2&+%`6-_8FKonG~yv}+s zKgUqRapwtxS_6ycXi>mk;dYy9cNyInd5$Rc%f>3R{d3mV9e#2eWBshPtXCVbF;B_d zwai2bm1n+^xv{Z#%=nJ0oDH@^LlTkbwA|d~{c{(C>S#vEU=SUNU@l_u)73(8@e6Qm zPV`g8;kKO_T4&y-yAJ4a_L$QEK75@<$fPL5pq%XN5=RUjyQcsE#WK8PsPPH8 zsFlo}jqNTL(;Nz_WL6DC`pvPjR93#O3rpBG5qEF!l#YZ0lVmVZ&ut<&8iue2!z(K*z(RgS&{&+UCtgiD!Fvr{W0ZV+5Si|6K0vM1 zwigY)YU=8(csh6U&U8Rcy^xhG)o=q~`KURMj(U<2y9Uh4ik`uPt}Ex`EVb(p7%2h4 z1d<|2adE=urmSR23R^0Y2qN;tM8wq`kx%@U#k0_SmNJ=pUvaFLsxA#K!2g6LUrquE zZE*xHUv7ES6m;Z9s)#~3#!oX2umxOstgH36$3u;#e12|jwUbWz+ZpKy@!anmsRKVf zYP&FYV7<~09|ZXMQRceN4aTY}v4NP46YfktBU7V*XM(ciXT_kJDyQ=q26rtE(iIG5 z;VQwsXZsq#o4+Y(q|B};e0^Q_q9XY?FUNP$(wI0B1E|9X*=-ko@}j;?VZTR$qT9jv z#Jfg5*DZ}@7Ts1MP2XYQsXKzZ@TxE2ha^48j4Wi|+jZR(pr)ds+Ft0ezk1sYFCE#!5Wlr?1Dca#oH_s+q(=g}A8@H^n(ewMGr`PNL63Yh>z{M; z^B)6Rf5P`fubfPhao7a6;ssO$Uic0UuIvD7SczZfNOc5OGGkUtfs=d4LMJxs_uwRI znw}T)qgTq!&~w7;Y8TRAHyqt1;|qjlHjtlIL5M~5%!FLRuQCOQzoz|!vZ2z0_CTe$ z%B5`5F*GGhU6?#!OJ$qFFYf)Xo}lB)tI4pifD@%uUo4mcIMxSmd=*p&LY!yYF;@1Y z`_*3Ct61Gxb>`3uXdg1>HaO2$j1fcTNn1)c7+sX(sV`e^|NFTmLapSM44Y*3N_AgjxPCAJvAUqoWb`FSo~kgR@JV zFI+uD#@0BmXo17uIM(V$)n5V*qM~r%$afF!?7RvKMwLt14QJJEy`_-#r|KGzA|OzT zwolw>f{Y&pI3XtbT{Fr3pH&Gz&K>g66)r~Imv0Tv*|~%bW3L?6)@vhvrbk?VuPi$Q zlbV}QOy2imSu~R@rD&+~MRW#YvHN-qz6|XDxc}C7mI&hyX3Nokb0Pj0-J2GpA>>yu<$?C;f0WCl0Pka|IAqa z8{+Msy2~}a|AN#EF_7Ho9&->Gi@?wPyvso$;LZ9Tq5L;%F$d8c5b6(QdMPwhK?ZP! zI(Pxz)PWm`pbjD`gZMsyrud^j{2YM|*}ouCZGHsd$vTBDHyEtAUIm8AMC1xaM~O@3 zIBzmVr0xTZkUos`bH5F6=m3BJb6J8uK%K~QusP()R^Lp~-cN*Hl!Kj$YDGOS$;ixW zCZL>`k<1yMQ=>L}P#FQhCaIG?SUydWCqNDR!E(Z5FABIMRHGWZwa3Ak%V|5KAzpAh zs$0x?BYr6MP|_^@@*q2-$>V7;(vef6pNF^Lmq*j|COwZ`Dr$9P2r>MzTpuL@0N2J) z4&m-VBgZ0*zSXVIN`DHRK5$JS_kUIzL-+-tw)JQi0IWq#h;ig194oo##HvN5F(*-? zSiZzrUV0S3QnYi(a`wiDsN*xJ*lk|21oj*eVn&N-;fowc<)$A6P%%2%)%|Mku^Rgg=T ze-^WT;Oz}S_VNI5C7KPO)p4?S56&*m7iDT1sYwpwf5EPOO8dJo70BL^QRIzlU(O$6?? z7l}ANJs_B{9ov&X b*Oy_f0_($f-aZJyORFre0jhmr`R;!J-v_Vi literal 0 HcmV?d00001 diff --git a/nuget/FreeRedis.Abp.Localization/images/image-20231209085657772.png b/nuget/FreeRedis.Abp.Localization/images/image-20231209085657772.png new file mode 100644 index 0000000000000000000000000000000000000000..8b7e4c2a16d95e7af268dd5df085c9b09c9a8959 GIT binary patch literal 14123 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 literal 0 HcmV?d00001