受欢迎的博客标签

How to Building a Multi-Lingual ASP.NET Core 5.x web Application localization with mongodb step by step

Published


This article will also demonstrate how the ASP.NET Core framework can be modified to match your requirements, creating new conventions and service implementations that you can easily include when composing your application.

Table of Contents

Where to store Localizing string?

How to configure localized string from Mongodb?

How to get localized string from Mongodb?

Defining the allowed cultures

Setting the Request Culture

 
Language Switcher

 

Create the MongodbStringLocalizerFactory class

Inject MongodbStringLocalizerFactory class

 

Where to store Localizing string?

多语言架构一般使用资源文件resx、XML、json,存储数据库来实现

 

How to configure localized string from Mongodb?

 IStringLocalizerFactory默认的实现是 ResourceManagerStringLocalizerFactory ,读取资源文件均是这个类实现来操作。

假设我要使用 json 文件、数据库 代替 resx 文件。  有2种方法:

1)只要实现对应的 IStringLocalizerFactory 并且代替默认的 ResourceManagerStringLocalizerFactory 。

2)重写 ResourceManagerStringLocalizerFactory 

 

Step 2: Create the MongodbStringLocalizerFactory class

IStringLocalizerFactory Interface:Represents a factory that creates Microsoft.Extensions.Localization.IStringLocalizer instances.

 

other:

IStringLocalizerFactory Interface  source code:

https://github.com/aspnet/Localization/blob/master/src/Microsoft.Extensions.Localization.Abstractions/IStringLocalizerFactory.cs

https://github.com/dotnet/aspnetcore/blob/master/src/Localization/Abstractions/src/LocalizedString.cs

 

NullStringLocalizerFactory+NullLocalizer

https://github.com/OrchardCMS/OrchardCore/src/OrchardCore/OrchardCore/Localization/NullStringLocalizerFactory.cs

 

 

Step 3: Create the MongodbStringLocalizer class 

IStringLocalizer是一个本地化字符串访问器的泛型接口,我们可以通过IStringLocalizer的属性访问器获取到对应字段在不同语言下的文本。

How to get localized string from Mongodb?

*.cstml

@inject IViewLocalizer Localizer
<span>@Localizer["ResourceKey.Welcome"], Ronald</span>

@Localizer["ResourceKey.Welcome"]->call IStringLocalizerFactory->call MongodbStringLocalizer class->find and return key-vlue from mongodb 

From the view (using IViewLocalizer)
From code (using IStringLocalizer<HomeController> via the controller)

 

ASP.NET Core: Defining the allowed cultures

ASP.Net Core provides a localization specific middleware which can be added to the pipeline by calling app.UseRequestLocalization.

When you add this middleware to your request pipeline, you can provide an object defining the cultures available in your application, plus the default culture.

As you will soon see in the code, there are separated lists for the Culture and for the UICulture. For those of you new to .NET and don’t know the difference between them:

  • · Culture is used when formatting or parsing culture dependent data like dates, numbers, currencies, etc
  • · UICulture is used when localizing strings, for example when using resource files.

You will also see specific cultures and neutral cultures. For example, the culture “en” is the neutral English culture, while “en-US”, “en-GB” and “en-AU” are the English specific cultures for the United States, Great Britain and Australia.

It’s probably time to see some code. Adding the following lines to your Startup.Configure method will define localization support for English and Spanish neutral cultures, with additional support for the United States and Spain specific cultures:

var supportedCultures = new[]
{
    new CultureInfo("en"),
    new CultureInfo("en-US"),
    new CultureInfo("es"),
    new CultureInfo("es-ES")
};
app.UseRequestLocalization(new RequestLocalizationOptions
{
    DefaultRequestCulture = new RequestCulture("en-US"),
    SupportedCultures = supportedCultures,
    SupportedUICultures = supportedCultures
});

注: SupportedCultures指定的是数字和日期格式;
SupportedUICultures指定的翻译文件

You might be wondering now how a culture from that list is selected when processing a request...

Setting the Request Culture

In the previous section, we have seen how to define the available cultures, but how is one of these selected for each request?

When you use app.UseRequestLocalization you are not just defining the available cultures, you are also defining the RequestCultureProviders.

These objects inspect the incoming request and try to find out which culture should be used by inspecting the request, each different provider following a different strategy.

  • The providers are evaluated in order; the culture will be taken from the first one that is able to determine it.
  • Once a provider determines a culture specified in the request, it will then be crossed with the available cultures before finally selecting the culture for that request.
  • If a specific culture which isn’t available was determined, the generic culture will be used if possible. Otherwise the default culture will be used.
  • If no provider is able to determine the culture, then the default culture will be used.

The predefined list of providers (which you can change) found in the RequestLocalizationOptions contains the following objects:

  • QueryStringRequestCultureProvider. Looks for the query string parameters culture and ui-culture. If only one is found, then that value is used for both the Culture and UICulture.
  • CookieRequestCultureProvider. Looks for a specific cookie which can be set with the static method CookieRequestCultureProvider.MakeCookieValue and whose default name “AspNetCore.Culture” is taken from CookieRequestCultureProvider.DefaultCookieName.
  • AcceptLanguageHeaderRequestCultureProvider. Looks at the Accept-Language header in the request.

These default settings might be enough for your application, especially if you provide some UX where users can pick the culture, triggering an action on your server that sets the AspNetCore.Culture cookie.

However it is also entirely possible to create your own RequestCultureProvider, for example one that uses a parameter from the url…

Creating a custom NopMongodbRequestCultureProvider

In many cases you might want the request to be specified in the url, as in:

 services.Configure<RequestLocalizationOptions>(options =>
                {
                    var supportedCultures = new[]
                    {
                    new CultureInfo("en"),

                    new CultureInfo("zh")
                    };

                    // State what the default culture for your application is. This will be used if no specific culture
                    // can be determined for a given request.
                    options.DefaultRequestCulture = new RequestCulture(culture: "zh", uiCulture: "zh");


                    // You must explicitly state which cultures your application supports.
                    // These are the cultures the app supports for formatting numbers, dates, etc.
                    options.SupportedCultures = supportedCultures;

                    // These are the cultures the app supports for UI strings, i.e. we have localized resources for.
                    options.SupportedUICultures = supportedCultures;

                    ////注释掉NopMongodbRequestCultureProvider(),缺省设置为en造成语言无法变化 2019/05/12 by freeman
                    options.RequestCultureProviders.Insert(0, new NopMongodbRequestCultureProvider());
                    // options.RequestCultureProviders.Add( new NopMongodbRequestCultureProvider());




                });

 

RequestCultureProvider Order

This is not possible with the default providers; however you can create your own implementation and add it to the list of providers in the RequestLocalizationOptions. The pipeline of request providers would then look like:

aspnet-core-request-providers

Figure 1. Pipeline of culture providers

You are setting "arabic" as DefaultRequestCulture but DefaultRequestCulture is used if none of the built-in providers can determine the request culture. The default providers are:

1.QueryStringRequestCultureProvider
2.CookieRequestCultureProvider
3.AcceptLanguageHeaderRequestCultureProvider

QueryStringRequestCultureProvider

ASP.NET Core会从URL中的culture参数中获取当前应用使用的语言文化,除了指定ui-culture参数,你还可以使用culture参数指定当前格式化时间,数字等所使用的语言文化。

?culture=zh-CN&ui-culture=zh-CN
?culture=zh-CN
?ui-culture=zh-CN

当只指定culture或ui-culture参数时,ASP.NET Core会自动将culture和ui-culture设置成一样的。即?culture=zh-CN等同于?culture=zh-CN&ui-culture=zh-CN

CookieRequestCultureProvider

ASP.NET Core中支持使用Cookie的方式设置当前应用使用的语言文化。默认使用的Cookie名称是.AspNetCore.Culture
.AspNetCore.Culture的值格式如下:

c=zh-CN|uic=zh-CN
c=zh-CN
uic=zh-CN

其中c表示culture, uic表示ui-culture

AcceptLanguageHeaderRequestCultureProvider

ASP.NET Core支持在请求头中指定语言文化。请求头中语言文化字段名称是 Accept-Language

 

Most likely the culture is determined from the Accept-Language HTTP header that the browser is sending.

You have to remove the AcceptLanguageHeaderRequestCultureProvider in order to fallback to DefaultRequestCulture. To do that, we can overwrite the RequestCultureProviders list of RequestLocalizationOptions and use only the other two providers. In Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    CultureInfo[] supportedCultures = new[]
    {
        new CultureInfo("ar"),
        new CultureInfo("en")
    };

    services.Configure<RequestLocalizationOptions>(options =>
    {
        options.DefaultRequestCulture = new RequestCulture("ar");
        options.SupportedCultures = supportedCultures;
        options.SupportedUICultures = supportedCultures;
        options.RequestCultureProviders = new List<IRequestCultureProvider>
        {
            new QueryStringRequestCultureProvider(),
            new CookieRequestCultureProvider()
        };
    });
}
 
Define the routes

Let’s start by defining an additional culture aware route that will contain a non-optional segment for the culture string:

  • While the default route matches urls like /controller/action/id, the additional culture aware will match urls like /culture/controller/action/id.
  • The culture segment will have a regex constraint so only valid culture strings like “en” or “es-ES” are allowed.
  • It should be added before the default route so it is evaluated first. Otherwise the culture string would be taken as the controller name!

The route definition will look like:

routes.MapRoute(
    name: "cultureRoute",
    template: "{culture}/{controller}/{action}/{id?}",
    defaults: new { controller = "Home", action = "Index" },                    
    constraints: new { 
       culture = new RegexRouteConstraint("^[a-z]{2}(?:-[A-Z]{2})?$") });

routes.MapRoute(
    name: "default",
    template: "{controller=Home}/{action=Index}/{id?}");

With these routes, your application would respond to urls like the following ones:

  • /
  • /en
  • /home
  • /es-ES/home
  • /home/index
  • /es/home/index
  • /home/index/123
  • /en-US/home/index/123

You also might want to consider attribute routing. It is less straightforward than using the route tables since routes are not defined in a central place.

  • One option would be making sure you add the culture segment to every api controller using attribute routing.
  • Another option would be creating a base controller class for your api controllers that use attribute routing, where you ensure the routes will always start by /api/culture/controller or /api/controller.
  • The better option would be creating your own convention for api controllers!

Let’s take a quick look at the base controller class option. This requires a base class like this:

[Route("api/{language:regex(^[[a-z]]{{2}}(?:-[[A-Z]]{{2}})?$)}/[controller]")]
[Route("api/[controller]")]
public class BaseApiController: Controller
{
}

..which you will then inherit from in your api controllers:

public class HelloWorldController: BaseApiController
{
    [HttpGet]
    public string SayHello()
    {
        return "Hello World";
    }
}

The endpoint SayHello above can be then reached with any of these urls:

  • /api/helloworld
  • /api/en/helloworld
  • /api/es-ES/helloworld

This might be enough, but it has a few problems:

  • You need to inherit from the base class on every api controller
  • The urls will always use the controller class names. If you want to redefine it and use something other than the controller name, you would then need to add route attributes to your controller but these attributes should contain the api and api/culture prefixes too!

Create a convention for api routes

A better solution of adding your own convention is not too complicated and is the preferred way for customizing ASP.Net Core in many scenarios.

aspnet-core-route-convention

Figure 2. Route convention compare to the base class approach

Custom conventions are created by implementing the interface IApplicationModelConvention, which exposes a single Apply method. The method receives an ApplicationModel object, which contains most of the metadata concerning your application like controllers, routes, actions, etc. This metadata can be altered within the convention’s Apply method, letting you creating your own conventions.

I will then create a new convention ApiPrefixConvention which will target all controllers whose class name ends with “ApiController” like HelloWorldApiController. This convention will either:

· Add the prefixes api/ and api/culture to any existing route attributes in that controller

· Add entire new route attributes api/controller and api/culture/controller if the controller does not have any specific route attribute

The convention will internally define a few AttributeRouteModel for either the route prefixes or the default routes; it will then proceed to update any controller whose name ends with ApiController:

public ApiPrefixConvention()
{
    //These are meant to be combined with existing route attributes
    apiPrefix = new AttributeRouteModel(
        new RouteAttribute("api/"));
    apiCulturePrefix = new AttributeRouteModel(
        new RouteAttribute("api/{language:regex(^[[a-z]]{{2}}(?:-[[A-Z]]{{2}})?$)}/"));

    //These are meant to be added as routes for api controllers that do not specify any route attribute
    apiRouteWithController = new AttributeRouteModel(
        new RouteAttribute("api/[controller]"));
    apiCultureRouteWithController = new AttributeRouteModel(
        new RouteAttribute("api/{language:regex(^[[a-z]]{{2}}(?:-[[A-Z]]{{2}})?$)}/[controller]"));
}

public void Apply(ApplicationModel application)
{
    foreach (var controller in application.Controllers.Where(c => c.ControllerName.EndsWith("Api")))
    {
        ApplyControllerConvention(controller);
    }
}

Applying the convention means checking whether the controller already defines any route attributes or not. If it has them, we need to combine them with the api/ and api/culture/ prefixes. Otherwise we will add the default route attributes api/controller and api/culture/controller. We will also remove the “Api” suffix from the controller name:

private void ApplyControllerConvention(ControllerModel controller)
{
    //Remove the "Api" suffix from the controller name 
    //The "Controller" suffix is already removed by default conventions
    controller.ControllerName = 
        controller.ControllerName.Substring(0, controller.ControllerName.Length - 3);

    //Either update existing route attributes or add new ones
    if (controller.Selectors.Any(x => x.AttributeRouteModel != null))
    {
        AddPrefixesToExistingRoutes(controller);
    }
    else
    {
        AddNewRoutes(controller);
    }
}

private void AddPrefixesToExistingRoutes(ControllerModel controller)
{
    foreach (var selectorModel in controller.Selectors.Where(x => x.AttributeRouteModel != null).ToList())
    {
        var originalAttributeRoute = selectorModel.AttributeRouteModel;
        //Merge controller selector with the api prefix
        selectorModel.AttributeRouteModel = 
            AttributeRouteModel.CombineAttributeRouteModel(apiPrefix,
                originalAttributeRoute);

        //Add another selector with the culture api prefix
        var cultureSelector = new SelectorModel(selectorModel);
        cultureSelector.AttributeRouteModel = 
            AttributeRouteModel.CombineAttributeRouteModel(apiCulturePrefix,
                originalAttributeRoute);
        controller.Selectors.Add(cultureSelector);
    }
}

private void AddNewRoutes(ControllerModel controller)
{
    //The controller has no route attributes, lets add a default api convention 
    var defaultSelector = controller.Selectors.First(s => s.AttributeRouteModel == null);
    defaultSelector.AttributeRouteModel = apiRouteWithController;
    //Lets add another selector for the api with culture convention
    controller.Selectors.Add(
        new SelectorModel { AttributeRouteModel = apiCultureRouteWithController });
}

With the convention finished, all left to do is adding the convention to the MVC options:

services.AddMvc(opts => opts.Conventions.Insert(0, new ApiPrefixConvention()))

Once everything is in place, let’s revisit our small sample api controller and rename it as HelloWorldApiController so it matches our new convention:

public class HelloWorldApiController
{
    [HttpGet]
    public string SayHello()
    {
        return "Hello World";
    }
}

As you can see, we don’t need to inherit from any base class. As it does not define any route attribute, its endpoint SayHello would then be accessible with any of these routes:

· /api/helloworld

· /api/en/helloworld

· /api/es-ES/helloworld

This is the same we achieved with the base class. However, we can also update the controller and add a new route attribute like:

[Route("hello-world")]
public class HelloWorldApiController
{
    …
}

Now our convention will detect there is a route attribute and will combine it with the api/ and api/culture/ prefixes. Now the SayHello endpoint will be accessible with any of these routes:

· /api/hello-world

· /api/en/hello-world

· /api/es-ES/hello-world

The convention approach gives you greater flexibility while it does not impose any constraints on how you should create your api controller classes. All you need to do is making sure you api controller names end with …ApiController.

Create the request provider

Right now, you application supports routes with a specific culture segment like /api/es-ES/hello-world. Let’s create a new RequestCultureProvider that inspects the url of the current request and looks for the culture segment.

All we need to do is implement the interface IRequestCultureProvider, which defines single method DetermineProviderCultureResult. In this method we need to find out if it is possible to get the culture from the request:

  • Inspect the request url for a culture parameter.
  • If a culture parameter is found, return a new ProviderCultureResult with that culture.
  • If no parameter is found, return null. This means the localization middleware will try with the next provider; if no provider is able to determine the culture, then the default will be used.

The implementation is not too complicated:

public class UrlRequestCultureProvider: IRequestCultureProvider
{
    public Task<ProviderCultureResult> DetermineProviderCultureResult(HttpContext httpContext)
    {
        var url = httpContext.Request.Path;

        //Quick and dirty parsing of language from url path, which looks like /api/es-ES/hello-world
        var parts = httpContext.Request.Path.Value
                     .Split('/')
                     .Where(p => !String.IsNullOrWhiteSpace(p)).ToList();
        if (parts.Count == 0)
        {
            return Task.FromResult<ProviderCultureResult>(null);
        }

        var cultureSegmentIndex = parts.Contains("api") ? 1 : 0;
        var hasCulture = Regex.IsMatch(
                  parts[cultureSegmentIndex], 
                  @"^[a-z]{2}(?:-[A-Z]{2})?$");
        if (!hasCulture)
        {
            return Task.FromResult<ProviderCultureResult>(null);
        }

        var culture = parts[cultureSegmentIndex];
        return Task.FromResult(new ProviderCultureResult(culture));
    }
}

As usual, once the provider is created, you just need to register it in the Startup class. Update the Configure method to add the new provider as the first provider in the LocalizationOptions:

localizationOptions.RequestCultureProviders.Insert(0,

new UrlRequestCultureProvider());

That’s it, with our provider defined and configured the culture for the request can be determined from the url, otherwise the next providers will be probed). For example:

  • /home/about will use the default culture
  • /es-ES/home/about will use the es-ES culture
  • /home/about?culture=es-ES will use the es-ES culture
  • /api/hello-world will use the default culture
  • /api/en/hello-world will use the en culture
  • /api/hello-world will use the en default culture assuming there is an Accept-Language header with that value

Localizing content

So far we have seen how the available cultures can be defined, and how one of those will be selected for each request. Now let’s take a look at how to provide content which has been localized to that culture.

ASP.Net Core contains services that can be used to find localized strings. They can be added to your application in Startup.ConfigureServices as in:

services.AddLocalization(options => options.ResourcesPath = "Resources");

services.AddMvc(opts => opts.Conventions.Insert(0, new ApiPrefixConvention()))
    .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
    .AddDataAnnotationsLocalization();

Localizing content in code

One of the most interesting services registered by the code above is the IStringLocalizer service. It can be used to find localized strings from any class. For example, we could update our simple HelloWorldApiController as:

public class HelloWorldApiController
{
    private readonly IStringLocalizer<HelloWorldApiController> localizer;
    public HelloWorldApiController(IStringLocalizer<HelloWorldApiController> localizer)
    {
        this.localizer = localizer;
    }

    [HttpGet]
    public string SayHello()
    {
        return this.localizer["HelloWorld"];
    }
}

The default implementation of IStringLocalizer will look for resource files based on the class name and the namespace of T. In the case of IStringLocalizer, assuming that controller is inside the Controllers namespace, the “en” culture resources file could be located in any of these locations:

  • /Resources/Controllers/HelloWorldApiController.en.resx
  • /Resources/Controllers.HelloWorldApiController.en.resx

As you can see, one option uses folders while the other uses dots to organize your files.

Localizing content in views

A similar service IViewLocalizer is provided to get localized contents in your views. This is injected using the directive @inject IViewLocalizer Localizer which you can then user in your view as in:

@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer
…

@Localizer["Message"]

The default implementation will too use resource files. It follows a similar strategy for finding the resources files for each view, based on its file path. The “en” culture resource file for the view “Views/Home/About.cshtml” can be located in any of these locations:

· /Resources/Views/Home/About.en.resx

· /Resources/Views.Home.About.en.resx

Alternatively, instead of using the IViewLocalizer service you can also create culture specific view files. This is done with the AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix) line of the StartupConfigureServices method.

Instead of creating different resource files for a particular view, you can directly create different views that contain localized content like About.es.cshtml and About.en-US.cshtml.

Conclusion

ASP.Net Core provides out of the box good support for Internationalization, which you can easily take advantage of when creating a new application.

As with the rest of the ASP.Net Core framework, it comes with sensible default options and implementations. When these defaults are not good enough, many times you can just use different options, others you will need to create your own implementations of the required services.

However extending or replacing the default functionality is not as hard as it seems. The UrlRequestProvider and ApiPrefixConvention created in this article are good examples of how to take advantage of the extension points provided by ASP.Net Core and implement your specific requirements in a very clean way.

One final word, there is more to it that what I could cover here! The asp docs site is a good starting point if you need to dig deeper.

Globalization and Localization in ASP.NET Core 3.x step by step