Globalization

Creating multilingual Web APIs in ASP.NET Core

In this article, I will teach you how to develop multi-language APIs in ASP.NET Core, through fundamentals and practical examples.

I’ll show you how to make your API return results in the user’s preferred language.

We will learn how to prepare the application for globalization and localization, how to create and manage resource files, how to use translations in the application code and finally, we will see language selection strategies.

Preparing your application for globalization

Setting up location services

Create a new project for our API in ASP.NET Core.

Configure the services necessary for globalization. When we talk about globalization we are referring to the process of making an application support different languages ​​and regions, date formats, currencies, etc.

builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");

«AddLocalization(…)» configures the services that we can later use in our components and controllers.

«ResourcesPath» specifies the folder that contains the resource files.

Location middleware configuration

«UseRequestLocalization()» is the middleware that allows us to manage the language and culture preferences of users and thus be able to adjust the application.

app.UseRequestLocalization();

Its main function is to detect and set the culture for each incoming HTTP request based on user preference, browser information, or any other custom logic that is defined, usually based on HTTP headers like Accept-Language and other options like QueryString or through cookies, which we will see later.

Setting location options

Use «builder.Services.Configure<RequestLocalizationOptions>(…)» to configure localization options.

builder.Services.Configure<RequestLocalizationOptions>(options =>
{
	var supportedCultures = new[] { "en-US", "es-ES" };
	options.SetDefaultCulture(supportedCultures[0])
		     .AddSupportedCultures(supportedCultures)
		     .AddSupportedUICultures(supportedCultures);
});

«RequestLocalizationOptions» is the class used to configure the localization options of the request.

«supportedCultures[…]» sets the default culture to be used if no culture preference is specified in the request.

«AddSupportedCultures(…)» sets the list of cultures supported by the application.

«AddSupportedUICultures(…)» sets the supported cultures for the user interface (UI).

Creating and managing resource files

When we talk about localization we are referring to the process of customizing an application for specific regions and languages. This involves the translation of texts and content into the supported languages.

We now create the resource files, which are the text strings for each culture, the culture is a language code and optionally a country or region code (“en”, “en-US”).

Language and country or region codes: <language code>-<country/region code>

Previously, we configured the “Resources” folder to contain the resource files.

Clarify that we can use the dot file naming:

Backend
├── Resources
│   ├── Controllers.AboutController.en-US.resx   (Inglés estadounidense)
│   ├── Controllers.AboutController.en.resx   (Inglés)
│   ├── Controllers.AboutController.es-ES.resx   (Español de España)
│   └── Controllers.AboutController.es.resx   (Español)
└── ...

or path file naming to organize resource files:

Backend
├── Resources
│   ├── Controllers
│       ├── AboutController.en-US.resx   (Inglés estadounidense)
│       ├── AboutController.en.resx   (Inglés)
│       ├── AboutController.es-ES.resx   (Español de España)
│       └── AboutController.es.resx   (Español)
└── ...

In our case, we will use path naming to organize the resource files for the different languages.

Once the resources are created, we open them and write the key value in the “name” column and the translated string in the “value” column:

We can now start using our resources in the controller of our Web API💪

Use of translations in the application code

Using IStringLocalizer to find strings of text

«IStringLocalizer<T>» allows us to use translations in the application code based on the current culture.

For our example, we will use it in the controller, injecting the service and using the text strings.

[ApiController]
[Route("api/[controller]")]
public class AboutController : ControllerBase
{
    private readonly IStringLocalizer<AboutController> _localizer;

    public AboutController(IStringLocalizer<AboutController> localizer)
    {
        _localizer = localizer;
    }

    [HttpGet]
    public string Get()
    {
        return _localizer["About Title"];
    }
}

Using translations in error messages

We can also use it to return the error messages that the API returns to the user or developer when a problem occurs, such as “Error 404: Resource not found” or “Authentication Error”, they may require translation if the API is intended for users or developers who speak different languages.

[ApiController]
[Route("api/[controller]")]
public class AboutController : ControllerBase
{
    ...
    [HttpGet("GetResource")]
    public IActionResult GetResource()
    {
        //error 404 not found
        return NotFound(_localizer["Resource not found"].Value);
    }
}

Using translations with DataAnnotations

We can adapt the validation messages and labels automatically generated by DataAnnotations validation annotations to different languages ​​and regions, allowing messages to be displayed according to users cultural preferences.

public class User
{
    [Required(ErrorMessage = "The Name field is required.")]
    [StringLength(50, ErrorMessage = "The Name field cannot be more than 50 characters.")]
    public string Name { get; set; }

    [Required(ErrorMessage = "The Email field is required.")]
    [EmailAddress(ErrorMessage = "The Email field does not have a valid format.")]
    public string Email { get; set; }
}

We need to add to program.cs method «AddDataAnnotationsLocalization(…)» which is used to configure the localization of validation messages based on Data Annotations in the application.

builder.Services.AddControllers()
    .AddDataAnnotationsLocalization(options => {
        options.DataAnnotationLocalizerProvider = (type, factory) =>
            factory.Create(typeof(SharedResource));
    });

«options.DataAnnotationLocalizerProvider» here, the data annotation localization provider is set. This means that when validation messages need to be localized, a shared resource (class SharedResource) will be used as the localization source.

We create the resource files and translation chains:

Lastly, we create the action in the controller that handles HTTP POST requests:

[Route("api/[controller]")]
[ApiController]
public class UserController : ControllerBase
{
    [HttpPost]
    public IActionResult Post([FromBody] User user)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        return Ok();
    }
}

Here it is verified whether the model "User" passed as a parameter complies with the validation rules defined in its class and if it does not comply, it displays the validation messages in the user’s language.

Shared resources between components

Shared Resources refer to resource files that contain text strings that are used in multiple components of an application. The idea behind shared resources is to centralize and reuse translations in different components of the application.

We create the shared resource files in the “Resources” folder:

Backend
├── Resources
│   ├── SharedResource.en-US.resx   (Inglés estadounidense)
│   ├── SharedResource.en.resx   (Inglés)
│   ├── SharedResource.es-ES.resx   (Español de España)
│   ├── SharedResource.es.resx   (Español)
│   └── ...
└── ...

We define the class «SharedResource» that is used as a marker for shared resources.

namespace Backend
{
	public class SharedResource
	{
	}
}

And we can now use it in the different application controllers:

[ApiController]
[Route("api/[controller]")]
public class HomeController : ControllerBase
{
    private readonly IStringLocalizer<SharedResource> _sharedLocalizer;

    public HomeController(IStringLocalizer<SharedResource> sharedLocalizer)
    {
        _sharedLocalizer = sharedLocalizer;
    }

    [HttpGet]
    public IActionResult Get()
    {
        return Ok(_sharedLocalizer["Your application shared resources."].Value);
    }
}

Language or culture selection strategies

Language refers to selections made by a user in browser settings, cookies, query settings, and other sources, but the application ultimately sets the CurrentCulture property of the user’s requested language.

«RequestCultureProviders» allow the application to determine the user’s preferred culture based on information provided by the browser, cookies, query parameters and other sources. We can configure multiple providers, keep in mind that the order in which these providers are added affects the priority with which the user’s preferred culture is chosen. They RequestCultureProviders are evaluated in the order in which they are registered in the app’s location settings.

Language detection via HTTP Accept-Language header

AcceptLanguageHeaderRequestCultureProvider this provider will determine the preferred culture based on the header Accept-Language in the browser request.

builder.Services.Configure<RequestLocalizationOptions>(options => {
    var SupportedCultures = new[] { "en-US", "es-ES" };
    options.SetDefaultCulture(SupportedCultures[0])
            .AddSupportedCultures(SupportedCultures)
            .AddSupportedUICultures(SupportedCultures)
            .RequestCultureProviders = new List<IRequestCultureProvider>
            {
                new AcceptLanguageHeaderRequestCultureProvider(),
                ...
            };
});

To test with the different languages, access the browser settings and change the language.

Language selection using QueryString

QueryStringRequestCultureProvider this provider will determine the preferred culture based on the values ​​of the query string of the URL.

.RequestCultureProviders = new List<IRequestCultureProvider>
{
   new QueryStringRequestCultureProvider(),
   ...
};

To change languages, modify the query string, for example, localhost/api/home?culture=en-US, localhost/api/home?culture=es-ES

CookieRequestCultureProvider this provider will determine the preferred culture based on a cookie in the request.

.RequestCultureProviders = new List<IRequestCultureProvider>
{
   new CookieRequestCultureProvider(),
   ...
};

To change the language, call the «SetLanguage» method

[HttpPost]
public IActionResult SetLanguage(string culture)
{
    Response.Cookies.Append(
        CookieRequestCultureProvider.DefaultCookieName,
        CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
        new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) }
    );

    return Ok();
}

To delete the Cookie, call «ClearLanguage»

[HttpDelete("ClearLanguage")]
public IActionResult ClearLanguage()
{
    Response.Cookies.Delete(CookieRequestCultureProvider.DefaultCookieName);
    return Ok();
}

Language selection via path

RouteDataRequestCultureProvider this provider will determine the preferred culture based on the URL path values.

.RequestCultureProviders = new List<IRequestCultureProvider>
{
   new RouteDataRequestCultureProvider(),
   ...
};
[Route("api/{culture}/[controller]")]
[ApiController]
public class RouteController : ControllerBase
{
    private readonly IStringLocalizer<RouteController> _localizer;

    public RouteController(IStringLocalizer<RouteController> sharedLocalizer)
    {
        _localizer = sharedLocalizer;
    }

    [HttpGet]
    public IActionResult Get()
    {
        return Ok(_localizer["The route language is {0}."].Value);
    }
}

To change the language, modify the route, for example, localhost/api/es/route, localhost/api/es-ES/route, localhost/api/en-US/route

Custom language selection

CustomRequestCultureProvider this custom provider could implement specific logic to determine the preferred culture.

.RequestCultureProviders = new List<IRequestCultureProvider>
{
    new CustomRequestCultureProvider(async context =>
    {
        // Here logic to determine the culture

        // This example simulates getting the culture "en-US" as the default.
        string culture = "es-ES";

        return new ProviderCultureResult(culture);
    }),
   ...
};

To test with the different ways of language selection, comment on providers and leave only the one you want to try. You can also try more than one provider, just keep the order in mind.

.RequestCultureProviders = new List<IRequestCultureProvider>
{
    new AcceptLanguageHeaderRequestCultureProvider(),
    new QueryStringRequestCultureProvider(),
    //new CookieRequestCultureProvider(),
    //new RouteDataRequestCultureProvider(),
    //new CustomRequestCultureProvider(async context => {...})
};

All implementations of these examples are available in the code on GitHub, which you can download.

That’s it, for now, I’ll keep updating and adding content in this post. I hope you found it interesting😉

Sample code

Localization in ASP.NET Core