现在项目中经常会要求支持本地化配置,实现本地化配置的方法有很多,例如微软官网提供了通过resx资源文件和PO文件实现本地化配置。但resx资源文件可扩展性不好,PO文件需要引入依赖包,本文主要介绍通过另一种可扩展的方式实现本地化。
.net core内置了本地化配置接口IStringLocalizer,接口具体方法如下:
public interface IStringLocalizer
{
/// <summary>
/// Gets the string resource with the given name.
/// </summary>
/// <param name="name">The name of the string resource.</param>
/// <returns>The string resource as a <see cref="LocalizedString"/>.</returns>
LocalizedString this[string name] { get; }
/// <summary>
/// Gets the string resource with the given name and formatted with the supplied arguments.
/// </summary>
/// <param name="name">The name of the string resource.</param>
/// <param name="arguments">The values to format the string with.</param>
/// <returns>The formatted string resource as a <see cref="LocalizedString"/>.</returns>
LocalizedString this[string name, params object[] arguments] { get; }
/// <summary>
/// Gets all string resources.
/// </summary>
/// <param name="includeParentCultures">
/// A <see cref="System.Boolean"/> indicating whether to include strings from parent cultures.
/// </param>
/// <returns>The strings.</returns>
IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures);
}
this[string name]索引器提供通过名称获取本地化配置文本;
this[string name, params object[] arguments]增加了参数arguments,用来做字符串占位符;
GetAllStrings(bool includeParentCultures)获取所有配置文本。
本地化配置修改好后,很少会修改。基于这个特点这里选择通过xml文件配置本地化语言,xml文件可扩展性较好,而且很方便后期在项目中扩展。实现主要包括两个对象:XmlLocalizerResource(xml资源对象)和XmlLocalizer(xml序列化对象)。
XmlLocalizerResource通过加载指定目录下的所有xml文件,反序列到懒加载字典对象Lazy<IDictionary<string, IDictionary<string, string>>>中,主要代码如下:
using MyWebDemo.Localizer;
using MyWebDemo.Utils;
namespace MyWebDemo
{
public class XmlLocalizerResource
{
private readonly IHostEnvironment _environment;
private readonly Lazy<IDictionary<string, IDictionary<string, string>>> _respurces;
public XmlLocalizerResource(IHostEnvironment environment)
{
_environment = environment;
_respurces = new Lazy<IDictionary<string, IDictionary<string, string>>>(InitializeResources);
}
public IDictionary<string, IDictionary<string, string>> Value
{
get
{
return _respurces.Value;
}
}
private IDictionary<string, IDictionary<string, string>> InitializeResources()
{
var infos = new Dictionary<string, IDictionary<string, string>>();
var files = Directory.GetFiles(Path.Combine(_environment.ContentRootPath, "xml"), "*.xml", SearchOption.AllDirectories);
foreach (var file in files)
{
var model = XmlUtil.Deserialize<XmlLocalizerModel>(file);
if (model == null)
{
continue;
}
if (string.IsNullOrWhiteSpace(model.Culture))
{
throw new ArgumentException("The language type is required!");
}
if (infos.ContainsKey(model.Culture))
{
throw new ArgumentException($"The language({model.Culture}) resource file is duplicated!");
}
infos.Add(model.Culture, model.Texts.ToDictionary(p => p.Name, p => p.Value));
}
return infos;
}
}
}
XmlLocalizer对象实现IStringLocalizer,具体代码如下:
using Microsoft.Extensions.Localization;
using System.Globalization;
using System.Text.RegularExpressions;
namespace MyWebDemo.Localizer
{
public class XmlLocalizer : IStringLocalizer
{
private readonly XmlLocalizerCulture _cultureInfo;
private readonly XmlLocalizerResource _resource;
public XmlLocalizer(XmlLocalizerCulture cultureInfo, XmlLocalizerResource resource)
{
_cultureInfo = cultureInfo;
_resource = resource;
}
public LocalizedString this[string name]
{
get
{
if (string.IsNullOrWhiteSpace(name))
{
return new LocalizedString(name, name);
}
else if (!_resource.Value.ContainsKey(_cultureInfo.Name) || !_resource.Value[_cultureInfo.Name].ContainsKey(name))
{
return new LocalizedString(name, GetDefaultString(name));
}
else
{
return new LocalizedString(name, _resource.Value[_cultureInfo.Name][name]);
}
}
}
public LocalizedString this[string name, params object[] arguments]
{
get
{
if (string.IsNullOrWhiteSpace(name))
{
return new LocalizedString(name, name);
}
else if (_resource.Value.ContainsKey(_cultureInfo.Name) || !_resource.Value[_cultureInfo.Name].ContainsKey(name))
{
return new LocalizedString(name, GetDefaultString(name));
}
else
{
return new LocalizedString(name, string.Format(_resource.Value[_cultureInfo.Name][name], arguments));
}
}
}
public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures)
{
if (includeParentCultures)
{
return _resource.Value.Values?.SelectMany(p => p.Select(t => new LocalizedString(t.Key, t.Value)))
?? new List<LocalizedString>();
}
return _resource.Value.ContainsKey(_cultureInfo.Name)
? _resource.Value[_cultureInfo.Name].Select(p => new LocalizedString(p.Key, p.Value))
: new List<LocalizedString>();
}
private string GetDefaultString(string name)
{
return Regex.Replace(name, "[a-z][A-Z]", m => m.Value[0] + " " + char.ToLower(m.Value[1], new CultureInfo(_cultureInfo.Name)));
}
}
}
测试获取HelloWorld本地化文本,返回正常,但每次序列化时都需要注册IStringLocalizer,代码碎片化严重,不利于维护,考虑添加在项目控制器基类中添加IStringLocalizer属性,并在基类中调用IStringLocalizer接口内方法,提供给子类控制器使用,具体代码如下:
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Primitives;
using MyWebDemo.Localizer;
namespace MyWebDemo.Controllers
{
[ApiController]
[Route("[controller]")]
public class ApiController : ControllerBase
{
protected IStringLocalizer Localizer
{
get
{
return HttpContext.RequestServices.GetRequiredService<IStringLocalizer>();
}
}
protected virtual string L(string name)
{
if (HttpContext.Request.Query.TryGetValue(XmlLocalizerCulture.Key, out StringValues val))
{
var cultureInfo = HttpContext.RequestServices.GetRequiredService<XmlLocalizerCulture>();
cultureInfo.Name = val;
}
return Localizer[name].Value;
}
}
}
考虑到实际项目中每次动态修改语言,所以需要支持动态配置功能,这里以sope生命周期注入XmlLocalizerCulture对象,属性包括语言名称Name,在项目初始化时默认为中文。在控制器基类中每次获取地址参数culture并赋值给XmlLocalizerCulture,实现动态配置功能。
完整的代码已添加附件,如有问题,欢迎指出!