.net core webapi implements localization

Nowadays, projects often require support for localized configuration. There are many ways to achieve localized configuration. For example, Microsoft’s official website provides localized configuration through resx resource files and PO files. However, the scalability of resx resource files is not good, and PO files need to introduce dependency packages. This article mainly introduces another scalable way to achieve localization.

.net core has a built-in localization configuration interface IStringLocalizer. The specific methods of the interface are as follows:

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);
}

The this[string name] indexer provides access to localized configuration text by name;

This[string name, params object[] arguments] adds parameters arguments, which are used as string placeholders;

GetAllStrings(bool includeParentCultures) gets all configuration text.

After the localization configuration is modified, it is rarely modified. Based on this feature, we choose to configure the localized language through the xml file. The xml file has good scalability and is very convenient for later expansion in the project. The implementation mainly includes two objects: XmlLocalizerResource (xml resource object) and XmlLocalizer (xml serialization object).

XmlLocalizerResource loads all xml files in the specified directory and deserializes them into the lazy loading dictionary object Lazy>>. The main code is as follows:

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;
}
}
}

The XmlLocalizer object implements IStringLocalizer. The specific code is as follows:

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 )));
        }
    }
}

The test obtains the localized text of HelloWorld and returns normal. However, IStringLocalizer needs to be registered every time it is serialized. The code is seriously fragmented and is not conducive to maintenance. Consider adding the IStringLocalizer attribute to the project controller base class and calling IStringLocalizer in the base class. The methods in the interface are provided for use by subclass controllers. The specific code is as follows:

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;
}
}
}

Considering that the language is dynamically modified every time in the actual project, it is necessary to support the dynamic configuration function. Here, the XmlLocalizerCulture object is injected through the sope life cycle. The attributes include the language name Name, which defaults to Chinese during project initialization. In the controller base class, the address parameter culture is obtained each time and assigned to XmlLocalizerCulture to implement the dynamic configuration function.

The complete code has been attached as an attachment. If there are any questions, please point them out!