前言

最近的新型冠状病毒流行让很多人主动在家隔离,希望疫情能快点消退。武汉加油,中国必胜!

Asp.Net Core 提供了内置的网站国际化(全球化与本地化)支持,微软还内置了基于 resx 资源字符串的国际化服务组件。可以在入门教程中找到相关内容。

但是内置实现方式有一个明显缺陷,resx 资源是要静态编译到程序集中的,无法在网站运行中临时编辑,灵活性较差。幸好我找到了一个基于数据库资源存储的组件,这个组件完美解决了 resx 资源不灵活的缺陷,经过适当的设置,可以在第一次查找资源时顺便创建数据库记录,而我们要做的就是访问一次相应的网页,让组件创建好记录,然后我们去编辑相应的翻译字段并刷新缓存即可。

但是!又是但是,经过一段时间的使用,发现基于数据库的方式依然存在缺陷,开发中难免有需要删除并重建数据库,初始化环境。这时,之前辛辛苦苦编辑的翻译就会一起灰飞烟灭 (╯‵□′)╯︵┻━┻ 。而 resx 资源却完美避开了这个问题,这时我就在想,能不能让他们同时工作,兼顾灵活性与稳定性,鱼与熊掌兼得。

经过一番摸索,终于得以成功,在此开贴记录分享。

正文

设置并启用国际化服务组件

安装 Nuget 包 Localization.SqlLocalizer,这个包依赖 EF Core 进行数据库操作。然后在 Startup 的 ConfigureServices 方法中加入以下代码注册  EF Core 上下文:

 services.AddDbContext<LocalizationModelContext>(options =>
     {
         options.UseSqlServer(connectionString);
     },
     ServiceLifetime.Singleton,
     ServiceLifetime.Singleton);

注册自制的混合国际化服务:

services.AddMixedLocalization(opts => { opts.ResourcesPath = "Resources"; }, options => options.UseSettings(true, false, true, true));

注册请求本地化配置:

 services.Configure<RequestLocalizationOptions>(
     options =>
     {
         var cultures =  Configuration.GetSection("Internationalization").GetSection("Cultures")
         .Get<List<string>>()
         .Select(x => new CultureInfo(x)).ToList();
         var supportedCultures = cultures;

         var defaultRequestCulture = cultures.FirstOrDefault() ?? new CultureInfo("zh-CN");
         options.DefaultRequestCulture = new RequestCulture(culture: defaultRequestCulture, uiCulture: defaultRequestCulture);
         options.SupportedCultures = supportedCultures;
         options.SupportedUICultures = supportedCultures;
     });

注册 MVC 本地化服务:

 services.AddMvc()
     //注册视图本地化服务
     .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix, opts => { opts.ResourcesPath = "Resources"; })
     //注册数据注解本地化服务
     .AddDataAnnotationsLocalization();

在 appsettings.json 的根对象节点添加属性:

"Internationalization": {
  "Cultures": [
    "zh-CN",
    "en-US"
  ]
}

在某个控制器加入以下动作:

 public IActionResult SetLanguage(string lang)
 {
     var returnUrl = HttpContext.RequestReferer() ?? "/Home";

     Response.Cookies.Append(
         CookieRequestCultureProvider.DefaultCookieName,
         CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(lang)),
         ) }
     );

     return Redirect(returnUrl);
 }

准备一个页面调用这个动作切换语言。然后,大功告成!

这个自制服务遵循以下规则:优先查找基于 resx 资源的翻译数据,如果找到则直接使用,如果没有找到,再去基于数据库的资源中查找,如果找到则正常使用,如果没有找到则按照对服务的配置决定是否在数据库中生成记录并使用。

自制混合国际化服务组件的实现

本体:

 public interface IMiscibleStringLocalizerFactory : IStringLocalizerFactory
 {
 }

 public class MiscibleResourceManagerStringLocalizerFactory : ResourceManagerStringLocalizerFactory, IMiscibleStringLocalizerFactory
 {
     public MiscibleResourceManagerStringLocalizerFactory(IOptions<LocalizationOptions> localizationOptions, ILoggerFactory loggerFactory) : base(localizationOptions, loggerFactory)
     {
     }
 }

 public class MiscibleSqlStringLocalizerFactory : SqlStringLocalizerFactory, IStringExtendedLocalizerFactory, IMiscibleStringLocalizerFactory
 {
     public MiscibleSqlStringLocalizerFactory(LocalizationModelContext context, DevelopmentSetup developmentSetup, IOptions<SqlLocalizationOptions> localizationOptions) : base(context, developmentSetup, localizationOptions)
     {
     }
 }

 public class MixedStringLocalizerFactory : IStringLocalizerFactory
 {
     private readonly IEnumerable<IMiscibleStringLocalizerFactory> _localizerFactories;
     private readonly ILogger<MixedStringLocalizerFactory> _logger;

     public MixedStringLocalizerFactory(IEnumerable<IMiscibleStringLocalizerFactory> localizerFactories, ILogger<MixedStringLocalizerFactory> logger)
     {
         _localizerFactories = localizerFactories;
         _logger = logger;
     }

     public IStringLocalizer Create(string baseName, string location)
     {
         return new MixedStringLocalizer(_localizerFactories.Select(x =>
         {
             try
             {
                 return x.Create(baseName, location);
             }
             catch (Exception ex)
             {
                 _logger.LogError(ex, ex.Message);
                 return null;
             }
         }));
     }

     public IStringLocalizer Create(Type resourceSource)
     {
         return new MixedStringLocalizer(_localizerFactories.Select(x =>
         {
             try
             {
                 return x.Create(resourceSource);
             }
             catch (Exception ex)
             {
                 _logger.LogError(ex, ex.Message);
                 return null;
             }
         }));
     }
 }

 public class MixedStringLocalizer : IStringLocalizer
 {
     private readonly IEnumerable<IStringLocalizer> _stringLocalizers;

     public MixedStringLocalizer(IEnumerable<IStringLocalizer> stringLocalizers)
     {
         _stringLocalizers = stringLocalizers;
     }

     public virtual LocalizedString this[string name]
     {
         get
         {
             var localizer = _stringLocalizers.SingleOrDefault(x => x is ResourceManagerStringLocalizer);
             var result = localizer?[name];
             if (!(result?.ResourceNotFound ?? true)) return result;

             localizer = _stringLocalizers.SingleOrDefault(x => x is SqlStringLocalizer) ?? throw new InvalidOperationException($"没有找到可用的 {nameof(IStringLocalizer)}");
             result = localizer[name];
             return result;
         }
     }

     public virtual LocalizedString this[string name, params object[] arguments]
     {
         get
         {
             var localizer = _stringLocalizers.SingleOrDefault(x => x is ResourceManagerStringLocalizer);
             var result = localizer?[name, arguments];
             if (!(result?.ResourceNotFound ?? true)) return result;

             localizer = _stringLocalizers.SingleOrDefault(x => x is SqlStringLocalizer) ?? throw new InvalidOperationException($"没有找到可用的 {nameof(IStringLocalizer)}");
             result = localizer[name, arguments];
             return result;
         }
     }

     public virtual IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
     {
         var localizer = _stringLocalizers.SingleOrDefault(x => x is ResourceManagerStringLocalizer);
         var result = localizer?.GetAllStrings(includeParentCultures);
         if (!(result?.Any(x => x.ResourceNotFound) ?? true)) return result;

         localizer = _stringLocalizers.SingleOrDefault(x => x is SqlStringLocalizer) ?? throw new InvalidOperationException($"没有找到可用的 {nameof(IStringLocalizer)}");
         result = localizer?.GetAllStrings(includeParentCultures);
         return result;
     }

     [Obsolete]
     public virtual IStringLocalizer WithCulture(CultureInfo culture)
     {
         throw new NotImplementedException();
     }
 }

 public class MixedStringLocalizer<T> : MixedStringLocalizer, IStringLocalizer<T>
 {
     public MixedStringLocalizer(IEnumerable<IStringLocalizer> stringLocalizers) : base(stringLocalizers)
     {
     }

     public override LocalizedString this[string name] => base[name];

     public override LocalizedString this[string name, params object[] arguments] => base[name, arguments];

     public override IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
     {
         return base.GetAllStrings(includeParentCultures);
     }

     [Obsolete]
     public override IStringLocalizer WithCulture(CultureInfo culture)
     {
         throw new NotImplementedException();
     }
 }

注册辅助扩展:

 public static class MixedLocalizationServiceCollectionExtensions
 {
     public static IServiceCollection AddMixedLocalization(
         this IServiceCollection services,
         Action<LocalizationOptions> setupBuiltInAction = null,
         Action<SqlLocalizationOptions> setupSqlAction = null)
     {
         if (services == null) throw new ArgumentNullException(nameof(services));

         services.AddSingleton<IMiscibleStringLocalizerFactory, MiscibleResourceManagerStringLocalizerFactory>();

         services.AddSingleton<IMiscibleStringLocalizerFactory, MiscibleSqlStringLocalizerFactory>();
         services.TryAddSingleton<IStringExtendedLocalizerFactory, MiscibleSqlStringLocalizerFactory>();
         services.TryAddSingleton<DevelopmentSetup>();

         services.TryAddTransient(typeof(IStringLocalizer<>), typeof(StringLocalizer<>));

         services.AddSingleton<IStringLocalizerFactory, MixedStringLocalizerFactory>();

         if (setupBuiltInAction != null) services.Configure(setupBuiltInAction);
         if (setupSqlAction != null) services.Configure(setupSqlAction);

         return services;
     }
 }

原理简介

服务组件利用了 DI 中可以为同一个服务类型注册多个实现类型,并在构造方法中注入服务集合,便可以将注册的所有实现注入组件同时使用。要注意主控服务和工作服务不能注册为同一个服务类型,不然会导致循环依赖。 内置的国际化框架已经指明了依赖 IStringLocalizerFatory ,必须将主控服务注册为 IStringLocalizerFatory,工作服只能注册为其他类型,不过依然要实现 IStringLocalizerFatory,所以最方便的办法就是定义一个新服务类型作为工作服务类型并继承 IStringLocalizerFatory。

想直接体验效果的可以到文章底部访问我的 Github 下载项目并运行。

结语

这个组件是在计划集成 IdentityServer4 管理面板时发现那个组件使用了 resx 的翻译,而我的现存项目已经使用了数据库翻译存储,两者又不相互兼容的情况下产生的想法。

当时 Localization.SqlLocalizer 旧版本(2.0.4)还存在无法在视图本地化时正常创建数据库记录的问题,也是我调试修复了 bug 并向原作者提交了拉取请求,原作者也在合并了我的修复后发布了新版本。

这次在集成 IdentityServer4 管理面板时又发现了 bug,正准备联系原作者看怎么处理。

转载请完整保留以下内容并在显眼位置标注,未经授权删除以下内容进行转载盗用的,保留追究法律责任的权利!

  本文地址:https://www.cnblogs.com/coredx/p/12271537.html

  完整源代码:Github

  里面有各种小东西,这只是其中之一,不嫌弃的话可以Star一下。

Asp.Net Core 混合全球化与本地化支持的更多相关文章

  1. 微软推出ASP.NET Core 2.0,并支持更新Visual Studio 2017

    微软推出ASP.NET Core 2.0的一般可用性,并发布.NET Core 2.0.该公司还推出了其旗舰集成开发环境(IDE)的更新:Visual Studio 2017版本15.3和Visual ...

  2. 体验 ASP.NET Core 中的多语言支持(Localization)

    首先在 Startup 的 ConfigureServices 中添加 AddLocalization 与 AddViewLocalization 以及配置 RequestLocalizationOp ...

  3. Asp.net Core 2.1 Kestrel 现在支持 多协议处理(Tcp)

    地址:https://github.com/davidfowl/MultiProtocolAspNetCore.git 在一个Kestrel服务上可以同时处理Tcp,Http,Https等多种协议. ...

  4. Asp.Net Core IdentityServer4 管理面板集成

    前言 IdentityServer4(以下简称 Id4) 是 Asp.Net Core 中一个非常流行的 OpenId Connect 和 OAuth 2.0 框架,可以轻松集成到 Asp.Net C ...

  5. ASP.NET Core 中文文档 第三章 原理(6)全球化与本地化

    原文:Globalization and localization 作者:Rick Anderson.Damien Bowden.Bart Calixto.Nadeem Afana 翻译:谢炀(Kil ...

  6. Jexus 5.8.2 正式发布为Asp.Net Core进入生产环境提供平台支持

    Jexus 是一款运行于 Linux 平台,以支持  ASP.NET.PHP 为特色的集高安全性和高性能为一体的 WEB 服务器和反向代理服务器.最新版 5.8.2 已经发布,有如下更新: 1,现在大 ...

  7. 一劳永逸:域名支持通配符,ASP.NET Core中配置CORS更轻松

    ASP.NET Core 内置了对 CORS 的支持,使用很简单,只需先在 Startup 的 ConfigureServices() 中添加 CORS 策略: public void Configu ...

  8. 在ASP.NET Core中如何支持每个租户数据存储策略的数据库

    在ASP.NET Core中如何支持每个租户数据存储策略的数据库 不定时更新翻译系列,此系列更新毫无时间规律,文笔菜翻译菜求各位看官老爷们轻喷,如觉得我翻译有问题请挪步原博客地址 本博文翻译自: ht ...

  9. 一劳永逸:域名支持通配符,ASP.NET Core中配置CORS

    ASP.NET Core 内置了对 CORS 的支持,使用很简单,只需先在 Startup 的 ConfigureServices() 中添加 CORS 策略: public void Configu ...

随机推荐

  1. [梁山好汉说IT] 容器概念在北宋社会的应用

    [梁山好汉说IT] 容器概念在北宋社会的应用 0x00 摘要 如何对没有软件开发经验的人解释容器? 集装箱真的能够完美解释容器嘛? 除了集装箱还有其他常见实体能够解释容器嘛? 我找到了一个能够 和集装 ...

  2. iptables 添加80端口规则

    iptables -t filter -A INPUT -p tcp -s 10.0.0.0/24 -j DROP 在filter表的input链做规则丢弃10.0.0.0网段的ip包iptables ...

  3. 洛谷$P2050\ [NOI2012]$美食节 网络流

    正解:网络流 解题报告: 传送门$QwQ$ 昂开始看到$jio$得,哇长得好像上一题嗷$QwQ$ 然后仔细康康数据范围,发现,哇好像要几万个点,,,显然就$GG$了 但感$jio$思路方向好对的亚子? ...

  4. spring cloud微服务快速教程之(三)声明式访问Feign、负载均衡Ribbon

    0-前言 eureka实际上已经集成了负载均衡调度框架Ribbon: 我们有了各个微服务了,那怎么来调用他们呢,一种方法是可以使用 RestTemplate(如:String str= restTem ...

  5. Spring的一些基本概念(面试备用哦)

    1.什么是Spring, 它有什么特点? 包括哪些内容? Spring是一个轻量级的控制反转(IoC)和面向切面(AOP)的容器框架. ◆  轻量——从大小与开销两方面而言Spring都是轻量的.完整 ...

  6. hutool BigExcelWriter 下的autoSizeColumnAll异常问题

    autoSizeColumnAll java.lang.IllegalStateException: Could not auto-size column. Make sure the column ...

  7. 【原创】Android adb错误“'adb' 不是内部或外部命令,也不是可运行的程序或批处理文件。”处理方法

    才刚刚接触Android没多久,现在使用adb命令的时候出现错误“'adb' 不是内部或外部命令,也不是可运行的程序或批处理文件.”,如下图所示: 这个问题一般有两种可能: 1.就是没有配置环境变量, ...

  8. Java项目之家庭收支记账软件

    模拟实现基于文本界面的家庭记账软件,该软件能够记录家庭的收入支出,并能够打印收支明细表. 项目采用分级菜单方式.主菜单如下: 假设家庭起始的生活基本金为10000元. 每次登记收入(菜单2)后,收入的 ...

  9. numpy nan和inf

    一.nan和inf的简介 nan 不是一个数字 读取本地文件为flaot的时候,有缺失 inf(infinity): 无穷尽 inf: 正无穷 -inf: 负无穷 数据类型:float # 注意: 要 ...

  10. Flash 上下文管理

    1.Local() 作用:为每个协程或线程创建一个独立的内存空间 储存格式: { 唯一标识: {'stack': []} } 代码 try: from greenlet import getcurre ...