Skip to content

LocalizationModule

The LocalizationModule provides basic features to work with localized resources for the cultures supported by the application under development.

The LocalizationModule sits on top of the features provided by .NET, this means that all the standard localization features can still be used to build up your applications.

Note

By now the LocalizationModule manages the localization resources related to backend tier, but in the future either backend or frontend resources will be managed by this module.

Localization concepts and components

The LocalizationModule is organized in Bundles, each bundle contains a set of resources related to a feature or module, for all the supported cultures. You can think of a bundle as a key-value dictionary of related resources required by your module.

Each localization Bundle is represented by an instance of ILocalizationResource, which in turns can be assigned a set of ILocalizationResourceContributor to contribute filling out the dictionary.

Each Bundle has attributes to define its characteristics, content and behavior:

  • An identifier: it is a Type used to uniquely identify the resource bundle. You will need to use this type identifier to refer to the resource bundle whenever you need to localize a resource out of it. The Bundle type identifier is accessible through the ResourceType attribute .

  • A contributor list: contributors fills out the resource bundle from a variety of sources, such as text files, databases, remote calls, etc. The contributor list is accessible through Contributors attribute of the ILocalizationResource instance.

  • A default culture: default culture will be used when no other culture matches the search criteria to localize a resource. The culture is set through the DefaultCultureName attribute of the ILocalizationResource instance.

  • Base resource collection: base resources act as parent localization source from which the resource bundle being defined is extending and / or overwriting preexistent resources. The base resource type collection is accessible through the BaseResourceTypes attribute of the ILocalizationResource instance.

Important

Every single message, text, label, etc., needed to be shown, sent, or displayed to the end user in your application must be done aware of the current culture. Thus, it must be obtained out of the proper IStringLocalizer instance.

Registering localization bundles

Each module must register its own bundle in the Resources collection of the LocalizationModuleOptions object provided by the Suite Framework, to be able to localize resources later on.

The following example shows the way to add and configure a new resource bundle:

C#
public class SomeModule : SuiteModule
{
    public override void SetupModule(IModuleBuilder builder)
    {
        builder.DependsOn<LocalizationModule, LocalizationModuleOptions>(
            options =>
        {
            // add module resources
            options.Resources
                .Add<SomeModuleResources>()
                .AddJsonEmbeddedDirectory(
                    @"EmbeddedResources",
                    this.GetType().Assembly)
                .AddJsonEmbeddedFile(
                    @"EmbeddedExtraResources\SomeModuleResources-es.json",
                    this.GetType().Assembly)
                .AddJsonDirectory(@"resources")
                .AddJsonFile(@"extra-resources\SomeModuleResources-en-US.json");

        });
    }
}

Some things to note from the example shown earlier:

  • The configuration is made by providing an action with the desired code block in SetupModule method of your ISuiteModule implementation.

  • The Add method available in LocalizationModuleOptions instance is requires a type parameter that acts as the module identifier.

  • In the example the AddJsonEmbeddedDirectory, AddJsonDirectory, AddJsonFile, and AddJsonEmbeddedFile methods add a JsonFileLocalizationContributor instance to fill out the localization dictionary. See here for further information on using Json files for localization.

  • Several contributors were added to the bundle being configured, each of one providing a culture specific localization resource set.

Note

Typically there will be one Bundle for each Suite Module, more instances are seldom needed, we will assume the case where only one bundle exists for each module for the rest of the document.

Localizing resources

Any time you want to provide a localized message you will need to obtain an IStringLocalizer instance and get the LocalizedString containing the localized resource for a given culture out of it.

IStringLocalizer instances are created by the IStringLocalizerFactory component. The default .Net Core Framework factory is replaced during bootstrapping with the one provided by the `Suite Framework to provide an enhanced version with the needed behavior to support our use cases.

The next example illustrates how to get an instance of IStringLocalizer injecting it in the constructor of an IAppService. This mechanism can be used to inject the desired IStringLocalizer instance out of DI in any other component or feature, such as controllers and services, for instance.

C#
public class JobsAppService : IJobsAppService
{
   private readonly IStringLocalizer localizer;

   public JobsAppService(IStringLocalizer<JobsAppModuleResources> localizer)
   {
       this.localizer = localizer;
   }

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

   public string SayHelloWithCurrentTime()
   {
       return this.localizer["MSG_GREETING_WITH_TIME", DateTime.Now.ShortTimeString];
   }
}

Some things to note from previous example:

  • The IStringLocalizer instance is injected by DI and referring to the JobsAppModuleResources resource bundle using its type parameter.

  • The IStringLocalizer<T> instance can be assigned to a more generic Type attribute, such as IStringLocalizer, for clarity and simplicity of code.

  • The IStringLocalizer instance is used to retrieve the culture aware resource identified by MSG_GREETING and MSG_GREETING_WITH_TIME names.

  • A given resource can be used to show plain texts or as templates whose placeholders are filled in with provided params at runtime (one or more parameters can be provided at once).

The localization will be performed using the CurrentUICulture of the current Thread. Please continue reading next sections for further information about this topic.

Note

An intent to localize a resource not contained in a Bundle will produce a non empty result always. Every localization operation ends up with an instance of LocalizedString, even if no resource can be found with the given key. A default response will be provided in such cases, and if you have to take care about the successful or not, you will then need to inspect the ResourceNotFound property of the result.

A more complex scenario might be supported by injecting multiple instances of IStringLocalizer pointing to a different Bundle each, or even injection IStringLocalizationFactory, which allows us to get instances of the desired IStringLocalizer for specific bundles. Let's see in the following examples.

Using multiple typed localizer instances:

C#
public class JobsAppService : IJobsAppService
{
   private readonly IStringLocalizer moduleLocalizer;
   private readonly IStringLocalizer localizer;

   public JobsAppService(
       IStringLocalizer<JobsAppModuleResources> moduleLocalizer,
       IStringLocalizer<SuiteCommonResources> commonResourcesLocalizer)
   {
       this.moduleLocalizer = moduleLocalizer;
       this.commonResourcesLocalizer = commonResourcesLocalizer;
   }

   public string SayHelloWithModuleLocalizer()
   {
       return this.moduleLocalizer["MSG_GREETING"];
   }

   public string SayHelloWithCommonLocalizer()
   {
       return this.commonResourcesLocalizer["MSG_GREETING"];
   }
}

Using the localizer factory:

C#
public class JobsAppService : IJobsAppService
{
   private readonly IStringLocalizerFactory localizerFactory;

   public JobsAppService(IStringLocalizerFactory localizerFactory)
   {
       this.localizerFactory = localizerFactory;
   }

   public string SayHelloWithModuleLocalizer()
   {
       return this.localizerFactory.Create(typeof(JobsAppModuleResources))["MSG_GREETING"];
   }

   public string SayHelloWithCommonLocalizer()
   {
       return this.localizerFactory.Create(typeof(SuiteCommonResources))["MSG_GREETING"];
   }
}

Determining the CurrentUICulture

The Localization module depends on the Localization middleware features provided by the ASP.Net Core Framework.

The Suite Framework relays in the request culture providers of the ASP.Net Core Localization Middleware. The way the framework assign defines which is the target culture is explained here

DefaultCulture and SupportedCultures assigned to the Suite Framework's LocalizationModule attributes at bootstrap time are used later to configure the underlying ASP.Net Core Localization Middleware. The request culture providers provided by the framework, out of the box, are then configured and used as well to define the current culture. The order in which the provides performs to determine the CurrentUICulture is as follows:

  1. QueryStringRequestCultureProvider: uses the QueryString parameters to determine the request culture.

  2. CookieRequestCultureProvider gets the current culture out of a cookie.

  3. AcceptLanguageHeaderRequestCultureProvider uses a well-known header value to determine the request culture.

LocalizationModule options

The LocalizationModule provides the following options to configure the feature and its behavior:

  • Resources: the collection of resource bundles for the application.
  • DefaultCulture: the default culture which is used by the application when a supported culture could not be determined by one of the configured Microsoft.AspNetCore.Localization.IRequestCultureProviders. By default the Suite Framework sets the en (english neutral) culture name.
  • SupportedCultures: the set of the supported cultures by the application.

The SupportedCultures and DefaultCulture play an important role when the current culture must be determined for a given incoming request, in conjunction with IRequestCultureProviders.

DefaultCulture and SupportedCultures values are used to configure the said and underlying ASP.Net Core Localization Middleware through RequestLocalizationOptions. For more information please refer to the Official .NET Documentation

Culture determination procedure (fallback)

Any time a Localized resource is requested for a Bundle, the following procedure is followed (the first not null outcome is returned):

  1. Contributors for the Bundle are invoked in the defined order, using the current culture.
  2. If not results were found with current culture, and if a parent culture exists for the current culture, the contributors will be invoked again for the parent culture.
  3. If we haven't succeeded yet finding the resource, if a DefaultCultureName was defined for the Bundle, the contributors will be invoked again for the Bundle's default culture.
  4. Finally base resource localizer instances, if any, will be invoked in its defined order, using the current culture initially, and culture fallback procedure mentioned here previously, if needed, afterwards.

Seamlessly integration with Resource (.resx) files

The LocalizationModule integrates seamlessly with .resx Resource files, such files can still be used for translation.

The recommended way to define Bundles when working with the Suite Framework is using LocalizationResourceDictionary, because this way the you will be able to extend from an existent Bundle, overriding existing, or adding new resources to it.

Any time a localized resource is requested, and the Bundle is not known by the LocalizationModule, the localization operation will be delegated to the underlying ResourceManagerStringLocalizerFactory, which will try to handle the localization procedure using Resource (.resx) files.
The ResourceManagerStringLocalizerFactory factory receives all the localization requests that cannot be handled using SuiteLocalizerFactory.

Note

When using resource files (.resx), the base is set by default to Resources. Take this into account because it makes the difference at the moment of deciding where to place the resource file (and its locale specific satellite assembly siblings for each supported culture). See here for more information about naming resource (.resx) files.

Using base types

Each Bundle can be assigned one or more Base types in order to inherit and extends, or even overwrite, the resources defined by base Bundles. In order to point to a base Bundle its identifier type must be used on the Bundle being extending it. Lets see an example:

C#
1
2
3
4
5
[InheritResource(typeof(SomeBaseResource))]
public class MyModuleResources
{

}

In the above example the Type identifying SomeBaseResource Bundle was used to indicate that MyModuleResources Bundle will use, extend or overwrite resources coming from SomeBaseResource Bundle. Think of this inheritance as a class hierarchy where ancestors provide resources than can be used by descendants. The inheritance is followed all along the hierarchy, thus if SomeBaseResource in turn has some inheritance definition, those localization resources will be available also for the MyModuleResources Bundle .

The next code snippet continues extends the previous example showing how the Bundle registration is done in a more complete code snippet.

Note

Take care of ancestors being registered as Bundles or it will produce a race condition when trying to configure and use the LocalizationModule, because of the reference made to the missing base Bundle.

C#
public void SetupModule(IModuleBuilder builder)
{
    builder.DependsOn<LocalizationModule, LocalizationModuleOptions>(options =>
    {
        // add base resources
        options.Resources
            .Add<SomeBaseResource>()
            .AddJsonFile(@"resources\baseResources-en.json").
            .AddJsonFile(@"resources\baseResources-es.json");

        // add module resources
        options.Resources
            .Add<MyModuleResources>()
            .AddJsonEmbeddedFile(
                @"Resources.MyModuleResources-en.json",
                this.GetType().Assembly);
    });
}

Remember that you can overwrite partially or none at all the inherited localization resources defined by base resources. Therefore, the following scenarios are valid, and its combination also:

  • Extend base resources with new ones, if you wan to extend previous definitions only.
  • Replace whole specific culture, if you want to replace the definitions only for an idiom or locale for example.
  • Replace some specific resource, if you need to do some sort of branding.

Using Json files for localization

When defining localized resources, a Json file can be used. Each Json file must provide resources only for one culture.

Note

By now, the only available contributor is JsonFileLocalizationContributor, which allows us to feed the Bundle instance from Json files. We expect to add more contributors in the feature.

Keep in mind that Json files will be read at runtime, therefore any kind of race condition will not be seen until the application is running.

Warning

Avoid to provide more than one file for the same culture, because that will produce an exception at runtime.

Json file structure

The structure of the Json file must follow a set of directives to be considered Well Formed and its content can be read by the framework. Below you can see an example of a valid Json localization resource file.

JSON
1
2
3
4
5
6
7
{
    "culture": "en",
    "texts": {
        "MSG_GREETING": "Suite Team says hello to you!",
        "MSG_CURRENT_DATETIME": "Current time is {0}"
    }
}

In the file there must be defined a culture attribute, and texts attribute to hold the collection of key/value pairs holding the localization resource name and value respectively.

Loading resources from assembly embedded files

To use json files embedded as assembly resources, you need to point to the correct embedded resource name. As embedded resources don't have a "directory" concept, AddJsonEmbeddedDirectory() will actually add all resources that start with the specified directory path, so you need to be careful to not add subdirectories by mistake. Also, as embedded resources don't allow directory separator characters in their names, the "/" and "\" characters will be mapped to the "." character, which is the way they are converted when embedded.

Note

Using embedded resources is preferred over using filesystem files, as this last option has caused conflicts in the past when different modules tried to add files to the "\resources" directory.

You need to modify your project's .csproj file to embed files in the assembly manifest. For example, to add all json files in the Resources directory to the assembly manifest, you should add this to your project's .csproj file:

XML
1
2
3
4
5
6
    <ItemGroup>
        <Content Remove="Resources/*.json" />
    </ItemGroup>
    <ItemGroup>
        <EmbeddedResource Include="Resources/*.json" />
    </ItemGroup>

Warning

Please be careful configuring the build action correctly so that the embedded resources are added to the assembly.

C#
public void SetupModule(IModuleBuilder builder)
{
    builder.DependsOn<LocalizationModule, LocalizationModuleOptions>(options =>
    {
        // add module resources
        options.Resources
            .Add<SomeModule>()
            .AddJsonEmbeddedDirectory(
                @"Resources",
                this.GetType().Assembly)
            .AddJsonEmbeddedFile(
                @"OtherResources/moduleResources-es.json",
                this.GetType().Assembly);
    });
}

Note

The culture name suffix present in the file name of the previous example is merely for illustration and clarity purpose, those suffixes will be ignored because the target culture is defined by the culture attribute inside each Json file.

Warning

Avoid using embedded resources that don't contain Json localization files only, because it can produce unwanted exceptions at runtime. Use this feature with caution.

Loading resources from files or directories

Json file contributors can be added through suitable extensions methods provided by the Suite framework. To use it you need to point to a valid Json path or directory. If the given path is not fully qualified, the path will be considered relative to the current executing assembly, and the resulting path will be the combination of both.

Warning

Please be careful configuring the build action correctly for all the Json files you want to use, because they must be present a runtime in the path you pointed at when adding to the Bundle.

C#
public void SetupModule(IModuleBuilder builder)
{
    builder.DependsOn<LocalizationModule, LocalizationModuleOptions>(options =>
    {
        // add module resources
        options.Resources
            .Add<SomeModule>()
            .AddJsonDirectory(@"resources").
            .AddJsonFile(@"extra-resources\moduleResources-es.json");
    });
}

Note

The culture name suffix present in the file name of the previous example is merely for illustration and clarity purpose, those suffixes will be ignored because the target culture is defined by the culture attribute inside each Json file.

In the previous example the resources will be loaded at runtime from the provided path relative to the executing assembly location:

Text Only
EXECUTING_ASSEMBLY_LOCATION\resources\

and

Text Only
EXECUTING_ASSEMBLY_LOCATION\resources\moduleResources-es.json

respectively.

Warning

Avoid pointing to a directory whose content doesn't contain Json localization files only, because it can produce unwanted exceptions at runtime. Use this feature with caution.

Localization for UI applications

The AspNetLocalizationModule along with LocalizationModule provide the features needed to localize UI applications leveraging the Suite's localization features.

To do so, have to create a module that depends on LocalizationModule and AspNetLocalizationModule altogether. Said module holds all the localizable resources required by the UI application to be exposed to the UI, which will be exposed through an http api, using a well known path.

The steps to follow are these:

  1. Create the UI localization module, you can leverage the suiteuilocalizationmodule template.
  2. Make your module depend on the LocalizationModule, adding your resource bundle to its options.
  3. Make your module depend on the AspNetLocalizationModule, exposing the bundle added in the previous step.
  4. Add your UI resources to the module, for all supported languages.
  5. Make your Application or BFF depend on the UI localization module. Add other UI localization modules, if needed.

Note

Most of this burden is done by the template, we encourage you to use it for bootstrapping the development process.

Step 1 to 3 are shown in the example here below.

C#
internal class MyModuleUiLocalizationModule : AspNetSuiteModule
{
    public override void SetupModule(IModuleBuilder builder)
    {
        base.SetupModule(builder);

        // Add bundle to the localization module
        builder.DependsOn<LocalizationModule, LocalizationModuleOptions>(opts =>
        {
            opts.Resources.Add<Resources>()
                .AddJsonEmbeddedDirectory("Resources", this.GetType().Assembly);
        });

        // Expose bundle through http api.
        builder.DependsOn<AspNetLocalizationModule, AspNetLocalizationModuleOptions>(opts =>
        {
            opts.ExposeResourceBundle<Resources>();
        });
    }
}

Naming convention

Given the fact that it's very likely to have to tweak the localization resources after build, perhaps because of branding, minor adjustments, or even to fix misspelled phrases or terms, we must be clear when naming the localization resources, to be able to distinguish clearly and decide correctly which one to change among all the ones contained in the resource bundle.

We encourage you to use this convention, it provides guidelines to name localization resource assets, giving the reader a decent hint, or context, on what the purpose of the resource is, and where is it supposed to be used, just by reading its key. After all, we don't have anything else but key to provide context for the resource being described.

The following are proposed rules for naming resources:

  • LBL_: use this prefix when the resource is going to be used to label an item which is might be bounded in size, such as menu items, tab titles, etc. LBL_ADMIN_ASSETS_HEADER
  • BTN_: same meaning as LBL_ but specifically used to denote an action rather than a noun, typically used to label buttons or links. For example: BTN_SAVE, or BTN_SAVE_CURRENT_ASSET.
  • MSG_: use this when the resource is used as text without sizing boundaries, such as text blocks or text paragraph in applications, being harmless if its size may vary when localized. For example: MSG_INVALID_USER_MESSAGE

Some things to note about this convention:

  • The names are in UpperSnakeCase
  • The suffix identify the purpose or context where the resource is used
  • The localized text can be either plain texts or templates

Keep in mind that in the future these definitions can vary to hold more scenarios or to fit some new features.

Note

There is no need to provide application id suffix or prefix, because the framework will do it for you, isolating resources located in each bundle. Keep the resource names simple, but yet meaningful.

Localized resources are limited by now to texts, no multimedia nor binary resources are supported today. If you wan to provide localization for those scenarios, you can use identifiers pointing to external assets (files, videos, blob storage, etc/) valid for the target culture.

Using localized templates

Localized resources can contain placeholders, which will be later filled in with provided parameters at runtime. The valid format and placeholders are those defined by String.Format

Info

The way we provide formatting for can be changed in the future, when adding
localization support for client applications that can be written in other languages and technologies other than C#.

Warning

When using templates avoid composing phrases combining several terms in your application, the reason is that for some languages the term position can vary, and the phrase can completely change its sense (or even make no sense at all) depending on the target language.