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...
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.
PM > Install-Package XLocalizer
PM > Install-Package XLocalizer.Translate
PM > Install-Package XLocalizer.Translate.MyMemoryTranslate
PM > Install-Package XLocalizer.TagHelpers
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.
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.
- 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");
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)
{
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));
});
services.AddHttpClient<ITranslator, MyMemoryTranslateService>();
services.AddSingleton<IXResourceProvider, XmlResourceProvider>();
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";
ops.UseExpressMemoryCache = true;
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
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):
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.
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
Supported Project Types
- Razor Pages
- MVC
- Blazor Server
References
History
- 13th November, 2020: Initial version