Asp.Net Core 混合全球化与本地化支持
前言
最近的新型冠状病毒流行让很多人主动在家隔离,希望疫情能快点消退。武汉加油,中国必胜!
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 混合全球化与本地化支持的更多相关文章
- 微软推出ASP.NET Core 2.0,并支持更新Visual Studio 2017
微软推出ASP.NET Core 2.0的一般可用性,并发布.NET Core 2.0.该公司还推出了其旗舰集成开发环境(IDE)的更新:Visual Studio 2017版本15.3和Visual ...
- 体验 ASP.NET Core 中的多语言支持(Localization)
首先在 Startup 的 ConfigureServices 中添加 AddLocalization 与 AddViewLocalization 以及配置 RequestLocalizationOp ...
- Asp.net Core 2.1 Kestrel 现在支持 多协议处理(Tcp)
地址:https://github.com/davidfowl/MultiProtocolAspNetCore.git 在一个Kestrel服务上可以同时处理Tcp,Http,Https等多种协议. ...
- Asp.Net Core IdentityServer4 管理面板集成
前言 IdentityServer4(以下简称 Id4) 是 Asp.Net Core 中一个非常流行的 OpenId Connect 和 OAuth 2.0 框架,可以轻松集成到 Asp.Net C ...
- ASP.NET Core 中文文档 第三章 原理(6)全球化与本地化
原文:Globalization and localization 作者:Rick Anderson.Damien Bowden.Bart Calixto.Nadeem Afana 翻译:谢炀(Kil ...
- Jexus 5.8.2 正式发布为Asp.Net Core进入生产环境提供平台支持
Jexus 是一款运行于 Linux 平台,以支持 ASP.NET.PHP 为特色的集高安全性和高性能为一体的 WEB 服务器和反向代理服务器.最新版 5.8.2 已经发布,有如下更新: 1,现在大 ...
- 一劳永逸:域名支持通配符,ASP.NET Core中配置CORS更轻松
ASP.NET Core 内置了对 CORS 的支持,使用很简单,只需先在 Startup 的 ConfigureServices() 中添加 CORS 策略: public void Configu ...
- 在ASP.NET Core中如何支持每个租户数据存储策略的数据库
在ASP.NET Core中如何支持每个租户数据存储策略的数据库 不定时更新翻译系列,此系列更新毫无时间规律,文笔菜翻译菜求各位看官老爷们轻喷,如觉得我翻译有问题请挪步原博客地址 本博文翻译自: ht ...
- 一劳永逸:域名支持通配符,ASP.NET Core中配置CORS
ASP.NET Core 内置了对 CORS 的支持,使用很简单,只需先在 Startup 的 ConfigureServices() 中添加 CORS 策略: public void Configu ...
随机推荐
- 洛谷$P1155$ 双栈排序 贪心+二分图匹配
正解:贪心+二分图匹配 解题报告: 传送门$QwQ$ 跪了,,,我本来以为我$NOIp$做得差不多了,,,然后康了一眼发现没做多少啊其实$QAQ$ 然后来康题趴$QwQ$ 首先考虑如果只有一个栈的情况 ...
- 多vps管理面板
iis7远程桌面连接工具,又叫做iis7远程桌面管理软件,是一款绿色小巧,功能实用的远程桌面管理工具,其界面简洁,操作便捷,能够同时远程操作多台服务器,并且多台服务器间可以自由切换,适用 ...
- 「洛谷P1402」酒店之王 解题报告
P1402 酒店之王 题目描述 XX酒店的老板想成为酒店之王,本着这种希望,第一步要将酒店变得人性化.由于很多来住店的旅客有自己喜好的房间色调.阳光等,也有自己所爱的菜,但是该酒店只有p间房间,一天只 ...
- 「Luogu P2508」[HAOI2008]圆上的整点 解题报告
题面 给定圆的半径,求圆上整点数 这是一道很Nice的数学题!超爱!好吧,由于这道题,我去Study了一下复数(complex number)复杂的数 真棒!!! 有兴趣的戳这里!!!\(\huge ...
- Java工作流系统-父子流程的配置讲解
父子流程 关键字: 驰骋工作流程快速开发平台 工作流程管理系统 工作流引擎 asp.net工作流引擎 java工作流引擎. 开发者表单 拖拽式表单 工作流系统 适配数据库: oralce,mysql ...
- Java江湖之设计模式
Java江湖之设计模式 ps:最近在学习设计模式,感觉这个是个装逼神器呀,就跟武功一样. 某日,senior同学看见rookie同学在练功. 问,"你练得什么武功?" ...
- Pandas常用方法手册
关键缩写和包导入 在这个速查手册中,我们使用如下缩写: df:任意的Pandas DataFrame对象 同时我们需要做如下的引入: import pandas as pd 导入数据 pd.read_ ...
- Javascript小白经典题型(一)
1. 输出是什么? function sayHi() { console.log(name) console.log(age) var name = 'Lydia' let age = 21 } sa ...
- key_load_public: invalid format
ssh-keygen -f ~/.ssh/id_rsa -y > ~/.ssh/id_rsa.pub
- 流程控制-物流费用计算(嵌套if)
题目描述 快递公司规定,如果物品体积超过2.5立方米,不允许快递.如果重量超过40kg,不允许快递.快递收费价格为: 小于等于1kg,一口价10块钱: 大于1kg,小于等于5kg,10块钱的基础上,每 ...