Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

ASP.NET Core Localization with XLocalizer

0.00/5 (No votes)
13 Nov 2020 1  
Localizing ASP.NET Core powered by online translation and auto resource creating...
Building a multi cultural web application requires a good understanding of localization infrastructure. I’ve previously shared a step-by-step tutorial to build multi-cultural ASP.NET Core web app. In this article, I will introduce a new nuget pacakge that also simplifies the localization setup and even eliminates the manually work for creating and filling localization resources.

ASP.NET Core localization, powered by online translation, auto resource creating and more...

Image 1

Introduction

Developing a multi cultural web application requires building the localization infrastructure that will process the request localization and localize views, error messages, …etc. On the other hand; every localized culture requires at least one resource file, filled with all localized key-value pairs.

Building the localization infrastructure and filling the resource files can take a lot of time and effort. XLocalizer has been developed from scratch to solve these two issues and free the developer from unnecessary workload.

What Does XLocalizer Offer?

Simple localization setup: First of all, it is built to help developers create localized web applications easily without wasting time on developing localization infrastructure.

Auto localization: The most attractive two features of XLocalizer are Auto resource creating and Online translation. So, any missed key will be translated and added automatically to the relevant resource file.

Support for multiple resource types: By default, ASP.NET Core uses “.resx” resource files for storing localized strings. XLocalizer breaks the barriers by offering built-in localization stores (XML, RESX, DB). Additionally, custom resource types for any other file or db format can be implemented.

Flexibility: XLocalizer is using the standard localization interfaces IStringLocalizer and IHtmlLocalizer, so it is easy to switch from the default .NET Core localization system to XLocalizer or vice versa. And with the help of built-in resource exports, all localization resources can be exported from any file/db type to “.resx” file type.

Customization: XLocalizer can be customized in every detail to

  • use custom resource types (e.g., mysql, json, csv, …etc.),
  • use custom resource exporters to export resources from any source to any source,
  • use custom translation services for translating resources.

Centralization: One single place to easily customize all validation errors, model binding erros and identity errors in a simple way.

In this tutorial, I will show how to use XLocalizer with XML resource files and online translation. To learn the setup of other resource types like resx, db or custom sources, kindly visit https://DOCS.Ziyad.info.

Installation

  • A few nugets are required to get the best of XLocalizer, first I will mention all packages, then we will see the role of each one in the next steps.
    // The main package
    PM > Install-Package XLocalizer
    
    // Online translation support
    PM > Install-Package XLocalizer.Translate
    
    // Translation service
    PM > Install-Package XLocalizer.Translate.MyMemoryTranslate
    
    // Use html tags to localize views
    PM > Install-Package XLocalizer.TagHelpers
    
    // Additional taghelper package for language dropdown
    PM > Install-Package LazZiya.TagHelpers
  • Resources folder: Under the project root, create a new folder named “LocalizationResources”, then inside it create a new empty class named “LocSource”. This class will be used to access the relevant resource files from the code.
    // Dummy class for grouping and accessing resource files
    public class LocSource { }

No need to create culture specific resource files, they will be created and filled automatically by XLocalizer.

Setup XLocalizer

A small tip to speedup coding; VS2019 can automatically insert missing namespaces (using …). Or you can press (Crtl + . ) to view a context menu that will add the missing namespace.

Image 2

  • Open startup file and configure request localization options as usual:
    services.Configure<RequestLocalizationOptions>(ops => 
    {
        var cultures = new CultureInfo[] { 
           new CultureInfo("en"),
           new CultureInfo("tr"),
           ...
        };
        ops.SupportedCultres = cultures;
        ops.SupportedUICultures = cultures;
        ops.DefaultRequestCulture = new RequestCulture("en");
    
        // Optional: add custom provider to support localization 
        // based on route value
        ops.RequestCultureProviders.Insert
            (0, new RouteSegmentRequestCultureProvider(cultures));
    });

    XLocalizer supports multiple resource types such as XML, RESX, DB, …etc. In this sample, I will use XML files to store localized values, so we need to register the built-inXmlResourceProvider, this provider will help us to use XML files as resource files for storing localized key-value pairs.

    services.AddSingleton<IXResourceProvider, XmlResourceProvider>();
  • One of the major benefits of XLocalizer is online translation support, so we need to register at least one translation service in startup file.
    services.AddHttpClient<ITranslator, MyMemoryTranslateService>();
    I used MyMemoryTranslateService during the development of XLocalizer, but you are free to choose any of the available translation services or even implement your own one.
  • Optionally, configure razor pages to use route based localization provider, so we can have the url like: http://localhost:111/en/Index. Then configure XLocalizer in the same step:
    services.AddRazorPages()
        .AddRazorPagesOptions(ops => 
        {
            ops.Conventions.Insert(0, new RouteTemplateModelConventionRazorPages());
        })
        .AddXLocalizer<LocSource, MyMemoryTranslateService>(ops => 
        {
            ops.ResourcesPath = "LocalizationResources";
            ops.AutoAddKeys = true;
            ops.AutoTranslate = true;
            ops.TranslateFromCulture = "en";
        });
  • Configure the app to use localization middleware:
    app.UseRequestLocalization();

Adding API Key for the translation Service

MyMemory translate APIs offers free anonymous usage till 1000 words/day (for the time of writing this story). So basically, you don’t have to add any key to test it. Anyhow, you can increase the free usage till 30.000 words/day just by providing an email and a freely generated key. See MyMemory API usage limits for more details.

Use MyMemory API Keygen to get a key, then add the key with a valid email address to user secrets file as below:

{   
  "XLocalizer.Translate": {
    "MyMemory": {
       "Email": "...",
       "Key": "..."
    }
  }
}

Different translation services may require different setup. See translation services docs for details about setup of different translation services.

Complete XLocalizer Startup Code

Sample startup file with unnecessary code omitted for simplification.

public class Startup
{    
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }
    
    // ...

    public void ConfigureServices(IServiceCollection services)
    {
        // Configure request localization
        services.Configure<RequestLocalizationOptions>(ops =>
        {
            var cultures = new CultureInfo[] { new CultureInfo("en"), 
            new CultureInfo("tr"), new CultureInfo("ar") };
            ops.SupportedCultures = cultures;
            ops.SupportedUICultures = cultures;
            ops.DefaultRequestCulture = 
                new Microsoft.AspNetCore.Localization.RequestCulture("en");
            ops.RequestCultureProviders.Insert
                (0, new RouteSegmentRequestCultureProvider(cultures));
        });

        // Register translation service
        services.AddHttpClient<ITranslator, MyMemoryTranslateService>();

        // Register XmlResourceProvider
        services.AddSingleton<IXResourceProvider, XmlResourceProvider>();

        services.AddRazorPages()
            .AddRazorPagesOptions(ops => 
            { ops.Conventions.Insert(0, new RouteTemplateModelConventionRazorPages()); })
            // Add XLocalizer
            .AddXLocalizer<LocSource, MyMemoryTranslateService>(ops =>
            {
                ops.ResourcesPath = "LocalizationResources";
                ops.AutoAddKeys = true;
                ops.AutoTranslate = true;
                
                // Optional: Just in case you need to change the source translation culture.
                // if not provided, the default culture will be used
                ops.TranslateFromCulture = "en";
                
                // Recommended: turn on caching during production for faster localization
                ops.UseExpressMemoryCache = true; 
            });
    }

    // This method gets called by the runtime. 
    // Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        // ...
        // Use request localization middleware
        app.UseRequestLocalization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapRazorPages();
        });
    }
}

That is all the setup required in startup file. Next, we will configure views and backend localization.

Localizing Views

We have installed a handy nuget for localizing views XLocalizer.TagHelpers, this package makes it easy to localize views using html tag and html attributes, which keeps the html code clean and easy to read and maintain.

  • Add the taghelper in _ViewImports.cshtml file:
    @addTagHelper *, XLocalizer.TagHelpers
  • Use localize-content attribute inside html tags to localize inner text/html:
    <h1 localize-content>Welcome</h1>
  • Localize inner text/html paragraph with localize html tag:
    <localize>
        <h1>Welcome</h1>
        <p>My contents...</p>
    </localize>
  • Localize html string with arguments:
    @{
        var args = new object[] { "http://DOCS.Ziyad.info" }
    }
    
    <p localize-args="args">
        Visit <a href="{0}">DOCS</a> for more details.
    </p>
  • Localize html attributes like title:
    <img src="../picture.jpg" localize-att-title="Nature picture" />

    Below is the fully localized sample of Register.cshtml page, notice that we only need to add “localize-content” attribute to the relevant tag, that keeps the page code clean and makes it easy to read and update.

    @page
    @model RegisterModel
    @{
        ViewData["Title"] = "Register";
    }
    
    <h1 localize-content>@ViewData["Title"]</h1>
    
    <div class="row">
        <div class="col-md-4">
            <form asp-route-returnUrl="@Model.ReturnUrl" method="post">
                <h4 localize-content>Create a new account.</h4>
                <hr />
                <div asp-validation-summary="All" class="text-danger"></div>
                <div class="form-group">
                    <label asp-for="Input.Email"></label>
                    <input asp-for="Input.Email" class="form-control" />
                    <span asp-validation-for="Input.Email" class="text-danger"></span>
                </div>
                <div class="form-group">
                    <label asp-for="Input.Password"></label>
                    <input asp-for="Input.Password" class="form-control" />
                    <span asp-validation-for="Input.Password" class="text-danger"></span>
                </div>
                <div class="form-group">
                    <label asp-for="Input.ConfirmPassword"></label>
                    <input asp-for="Input.ConfirmPassword" class="form-control" />
                    <span asp-validation-for="Input.ConfirmPassword" 
    
                     class="text-danger"></span>
                </div>
                <button type="submit" class="btn btn-primary" localize-content>
                 Register</button>
            </form>
        </div>
    </div>
    @section Scripts {
        <partial name="_ValidationScriptsPartial" />
    }
    View localization sample with XLocalizer.TagHelpers

Localizing Validation Attributes Errors, Model Binding Errors and Identity Error Messages

Localizing all framework error messages do not require any additional setup with XLocalizer, and it is not necessary to provide any error message inside the attribute tags! All error messages will be assigned and localized by the default setup of XLocalizer in startup.

Below is a sample use for some validation attributes:

[Required]
[EmailAddress]
[Display(Name = "Email")]
public string Email { get; set; }

Also for model binding errors and identity errors, we don’t have to do any additional setup, XLocalizer will take care of localizing all the error messages by default.

Below is a sample of backend localization of Register.cshtml.cs file:

public class InputModel
{
    [Required]
    [EmailAddress]
    [Display(Name = "Email")]
    public string Email { get; set; }

    [Required]
    [StringLength(100, MinimumLength = 6)]
    [DataType(DataType.Password)]
    [Display(Name = "Password")]
    public string Password { get; set; }

    [DataType(DataType.Password)]
    [Display(Name = "Confirm password")]
    [Compare("Password")]
    public string ConfirmPassword { get; set; }
}
Validation attributes usage without defining error messages

Customizing Error Messages

In some cases, you may need to customize the error messages for validation attributes, model binding or identity. Or you may want to provide the default error messages in a culture other than “en”, so XLocalizer can do translate from the correct culture.

The first solution is to use inline options setup in startup file, by providing the relevant error messages as below:

services.AddRazorPages()
        .AddXLocalizer<...>(ops =>
        {
            // ...
            ops.ValidationErrors = new ValidationErrors 
            {
                RequiredAttribute_ValidationError = "The {0} field is required.",
                CompareAttribute_MustMatch = 
                "'{0}' and '{1}' do not match.",
                StringLengthAttribute_ValidationError = 
                "The field {0} must be a string with a maximum length of {1}.",
                // ...
            };
            ops.ModelBindingErrors = new ModelBindingErrors 
            {
                AttemptedValueIsInvalidAccessor = "The value '{0}' is not valid for {1}.",
                MissingBindRequiredValueAccessor = 
                "A value for the '{0}' parameter or property was not provided.",
                MissingKeyOrValueAccessor = "A value is required.",
                // ...
            };
            ops.IdentityErrors = new IdentityErrors 
            {
                DuplicateEmail = "Email '{0}' is already taken.",
                DuplicateUserName = "User name '{0}' is already taken.",
                InvalidEmail = "Email '{0}' is invalid.",
                // ...
            };
        });
Customizing error messages in startup file

The other option is to configure all XLocalizer settings in a json file.

JSON Settings

If you are a developer who likes to keep startup file clean “like me :)” you will be happy to know that you can do all these customizations in a json file and use only one line to read the configurations in startup.

  • Add the relevant configuration to appsettings.json or any custom json file of your choice.
    {
        "XLocalizerOptions" : {
            "AutoAddKeys" : true,
            "AutoTranslate" : true,
            // ...
        }
    }
  • Setup XLocalizer to read the relevant configuration section:
    services.AddRaqzorPages()
            .AddXLocalizer<...>
            (ops => Configuration.GetSection("XLocalizerOptions").Bind(ops));
  • Below is a sample json settings for XLocalizer options with the customizable error messages:
    {
      "XLocalizerOptions": {
        "ResourcesPath": "LocalizationResources",
        "AutoAddKeys": true,
        "AutoTranslate": true,
        "UseExpressMemoryCache": true,
        "TranslateFromCulture": "en",
        "ValidationErrors": {
          "CompareAttribute_MustMatch": "'{0}' 
                and '{1}' do not match. They should not be different!",
          "CreditCardAttribute_Invalid": 
                "The {0} field is not a valid credit card number.",
          "CustomValidationAttribute_ValidationError": "{0} is not valid.",
          "DataTypeAttribute_EmptyDataTypeString": 
                "The custom DataType string cannot be null or empty.",
          "EmailAddressAttribute_Invalid": 
                "The {0} field is not a valid e-mail address.",
          "FileExtensionsAttribute_Invalid": 
                "The {0} field only accepts files with the following extensions: {1}",
          "MaxLengthAttribute_ValidationError": 
                "The field {0} must be a string or array type 
                 with a maximum length of '{1}'.",
          "MinLengthAttribute_ValidationError": 
                "The field {0} must be a string or array type 
                 with a minimum length of '{1}'.",
          "PhoneAttribute_Invalid": "The {0} field is not a valid phone number.",
          "RangeAttribute_ValidationError": 
                "The field {0} must be between {1} and {2}.",
          "RegexAttribute_ValidationError": 
                "The field {0} must match the regular expression '{1}'.",
          "RequiredAttribute_ValidationError": 
                "The {0} field is required. Don't bypass this field!",
          "StringLengthAttribute_ValidationError": 
                "The field {0} must be a string with a maximum length of {1}.",
          "StringLengthAttribute_ValidationErrorIncludingMinimum": 
                "The field {0} must be a string with a minimum length of {2}
                 and a maximum length of {1}.",
          "UrlAttribute_Invalid": "The {0} field is not a valid fully-qualified http,
                                   https, or ftp URL.",
          "ValidationAttribute_ValidationError": "The field {0} is invalid."
        },
        "IdentityErrors": {
          "DuplicateEmail": "Email '{0}' is already taken.",
          "DuplicateUserName": "User name '{0}' is already taken. 
                                Please try another one.",
          "InvalidEmail": "Email '{0}' is invalid.",
          "DuplicateRoleName": "Role name '{0}' is already taken.",
          "InvalidRoleName": "Role name '{0}' is invalid.",
          "InvalidToken": "Invalid token.",
          "InvalidUserName": 
                  "User name '{0}' is invalid, can only contain letters or digits.",
          "LoginAlreadyAssociated": "A user with this login already exists.",
          "PasswordMismatch": "Incorrect password.",
          "PasswordRequiresDigit": "Passwords must have at least one digit ('0'-'9').",
          "PasswordRequiresLower": 
                   "Passwords must have at least one lowercase ('a'-'z').",
          "PasswordRequiresNonAlphanumeric": 
                   "Passwords must have at least one non alphanumeric character.",
          "PasswordRequiresUniqueChars": 
                   "Passwords must use at least {0} different characters.",
          "PasswordRequiresUpper": 
                   "Passwords must have at least one uppercase ('A'-'Z').",
          "PasswordTooShort": "Passwords must be at least {0} characters.",
          "UserAlreadyHasPassword": "User already has a password set.",
          "UserAlreadyInRole": "User already in role '{0}'.",
          "UserNotInRole": "User is not in role '{0}'.",
          "UserLockoutNotEnabled": "Lockout is not enabled for this user.",
          "RecoveryCodeRedemptionFailed": "Recovery code redemption failed.",
          "ConcurrencyFailure": "Optimistic concurrency failure, 
                                 object has been modified.",
          "DefaultError": "An unknown failure has occurred."
        },
        "ModelBindingErrors": {
          "AttemptedValueIsInvalidAccessor": "The value '{0}' is not valid for {1}.",
          "MissingBindRequiredValueAccessor": 
                "A value for the '{0}' parameter or property was not provided.",
          "MissingKeyOrValueAccessor": "A value is required.",
          "MissingRequestBodyRequiredValueAccessor": 
                         "A non-empty request body is required.",
          "NonPropertyAttemptedValueIsInvalidAccessor": "The value '{0}' is not valid.",
          "NonPropertyUnknownValueIsInvalidAccessor": "The supplied value is invalid.",
          "NonPropertyValueMustBeANumberAccessor": "The field must be a number.",
          "UnknownValueIsInvalidAccessor": "The supplied value is invalid for {0}.",
          "ValueIsInvalidAccessor": 
                  "The value '{0}' is invalid. You entered something weird!",
          "ValueMustBeANumberAccessor": 
                "The field {0} must be a number. 
                 Don't use letters or special characters.",
          "ValueMustNotBeNullAccessor": 
                 "The value '{0}' is invalid. This can't be null."
        }
      }
    }
    Customizing all XLocalizer options in json file

So, this is a single place and simple way to customize all error messages. These messages will be translated to other cultures by XLocalizer depending on the request culture.

Adding Language Navigation

Each multi cultural web application must provide a way to switch between different languages. You may have your own implementation for language navigation, but just in case you need to add one easily (we’ve installed LazZiya.TagHelpers earlier):

  • Add taghelpers to _ViewImports file:
    @addTagHelper *, LazZiya.TagHelpers
  • Open _layout.cshtml and add the language navigation where you need it to appear:
    <language-nav></language-nav>

I highly recommend to setup the language navigation to configure culture cookie as described in the docs page here. So the culture choice can be stored in a cookie for later use.

Run the Application

If you have done all the steps correctly, and once you start the application, take a look at the output window in VS to see the logs, you will see that XLocalizer has started to translate the views and insert values automatically. Additionally, all validation attributes, model binding and identity error also localized.

Image 3

Sample screenshot of localized registration form
Notice: Localizing identity pages requires scaffolding the identity into the project.

All you need to add a new culture is; add the culture to the supported cultures in startup file, and keep all the rest to be done by XLocalizer. :)

Supported .NET Core Versions

  • 2.x
  • 3.x
  • 5.0

Supported Project Types

  • Razor Pages
  • MVC
  • Blazor Server

References

History

  • 13th November, 2020: Initial version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here