Configure portable object localization in ASP.NET Core

By Hisham Bin Ateya and Sébastien Ros.

This article walks through the steps for using Portable Object (PO) files in an ASP.NET Core application with the Orchard Core framework.

Note: Orchard Core isn't a Microsoft product. Microsoft provides no support for this feature.

View or download sample code (how to download)

What is a PO file?

PO files are distributed as text files containing the translated strings for a given language. Some advantages of using PO files instead of .resx files include:

  • PO files support pluralization; .resx files don't support pluralization.
  • PO files aren't compiled like .resx files. As such, specialized tooling and build steps aren't required.
  • PO files work well with collaborative online editing tools.

Example

The following sample PO file contains the translation for two strings in French, including one with its plural form:

fr.po

#: Pages/Index.cshtml:13
msgid "Hello world!"
msgstr "Bonjour le monde!"

msgid "There is one item."
msgid_plural "There are {0} items."
msgstr[0] "Il y a un élément."
msgstr[1] "Il y a {0} éléments."

This example uses the following syntax:

  • #:: A comment indicating the context of the string to be translated. The same string might be translated differently depending on where it's being used.
  • msgid: The untranslated string.
  • msgstr: The translated string.

For pluralization support, more entries can be defined.

  • msgid_plural: The untranslated plural string.
  • msgstr[0]: The translated string for the case 0.
  • msgstr[N]: The translated string for the case N.

The PO file specification can be found here.

Configuring PO file support in ASP.NET Core

This example is based on an ASP.NET Core Web application generated from a Visual Studio 2022 project template.

Referencing the package

Add a reference to the OrchardCore.Localization.Core NuGet package.

The .csproj file now contains a line similar to the following (version number may vary):

<PackageReference Include="OrchardCore.Localization.Core" Version="1.5.0" />

Registering the service

Add the required services to Program.cs:

builder.Services.AddPortableObjectLocalization();

builder.Services
    .Configure<RequestLocalizationOptions>(options => options
        .AddSupportedCultures("fr", "cs")
        .AddSupportedUICultures("fr", "cs"));

builder.Services
    .AddRazorPages()
    .AddViewLocalization();

Add the following code to your Razor page of choice. Index.cshtml is used in this example.

@page
@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer
@{
    ViewData["Title"] = "Home";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

<p>@Localizer["Hello world!"]</p>

An IViewLocalizer instance is injected and used to translate the text "Hello world!".

Creating a PO file

Create a file named <culture code>.po in your application root folder. In this example, the file name is fr.po because the French language is used:

msgid "Hello world!"
msgstr "Bonjour le monde!"

This file stores both the string to translate and the French-translated string. Translations revert to their parent culture, if necessary. In this example, the fr.po file is used if the requested culture is fr-FR or fr-CA.

Testing the application

Run your application, the text Hello world! is displayed.

Navigate to the URL /Index?culture=fr-FR. The text Bonjour le monde! is displayed.

Pluralization

PO files support pluralization forms, which is useful when the same string needs to be translated differently based on a cardinality. This task is made complicated by the fact that each language defines custom rules to select which string to use based on the cardinality.

The Orchard Localization package provides an API to invoke these different plural forms automatically.

Creating pluralization PO files

Add the following content to the previously mentioned fr.po file:

msgid "There is one item."
msgid_plural "There are {0} items."
msgstr[0] "Il y a un élément."
msgstr[1] "Il y a {0} éléments."

See What is a PO file? for an explanation of what each entry in this example represents.

Adding a language using different pluralization forms

English and French strings were used in the previous example. English and French have only two pluralization forms and share the same form rules, which is that a cardinality of one is mapped to the first plural form. Any other cardinality is mapped to the second plural form.

Not all languages share the same rules. This is illustrated with the Czech language, which has three plural forms.

Create the cs.po file as follows, and note how the pluralization needs three different translations:

msgid "Hello world!"
msgstr "Ahoj světe!!"

msgid "There is one item."
msgid_plural "There are {0} items."
msgstr[0] "Existuje jedna položka."
msgstr[1] "Existují {0} položky."
msgstr[2] "Existuje {0} položek."

To accept Czech localizations, add "cs" to the list of supported cultures in the Configure method:

builder.Services
    .Configure<RequestLocalizationOptions>(options => options
        .AddSupportedCultures("fr", "cs")
        .AddSupportedUICultures("fr", "cs"));

Edit the Pages/Index.cshtml file to render localized, plural strings for several cardinalities:

<p>@Localizer.Plural(1, "There is one item.", "There are {0} items.")</p>
<p>@Localizer.Plural(2, "There is one item.", "There are {0} items.")</p>
<p>@Localizer.Plural(5, "There is one item.", "There are {0} items.")</p>

Note: In a real world scenario, a variable would be used to represent the count. Here, we repeat the same code with three different values to expose a specific case.

Upon switching cultures, you see the following:

For /Index:

There is one item.
There are 2 items.
There are 5 items.

For /Index?culture=fr:

Il y a un élément.
Il y a 2 éléments.
Il y a 5 éléments.

For /Index?culture=cs:

Existuje jedna položka.
Existují 2 položky.
Existuje 5 položek.

For the Czech culture, the three translations are different. The French and English cultures share the same construction for the two last translated strings.

Advanced tasks

Using additional arguments

The argument at index zero {0} always represents the count value. When invoking the Plural method it is possible to add additional arguments and their index will then start at one (1).

<p>@Localizer.Plural(count, "There is one item with the color {1}.", "There are {0} items. The main color is {1}.", color)</p>

Contextualizing strings

Applications often contain the strings to be translated in several places. The same string may have a different translation in certain locations within an app (Razor views or class files). A PO file supports the notion of a file context, which can be used to categorize the string being represented. Using a file context, a string can be translated differently, depending on the file context (or lack of a file context).

The PO localization services use the name of the full class or the view that's used when translating a string. This is accomplished by setting the value on the msgctxt entry.

Consider a minor addition to the previous fr.po example. A Razor page located at Pages/Index.cshtml can be defined as the file context by setting the reserved msgctxt entry's value:

msgctxt "Views.Home.About"
msgid "Hello world!"
msgstr "Bonjour le monde!"

With the msgctxt set as such, text translation occurs when navigating to /Index?culture=fr-FR. The translation doesn't occur when navigating to /Privacy?culture=fr-FR.

When no specific entry is matched with a given file context, Orchard Core's fallback mechanism looks for an appropriate PO file without a context. Assuming there's no specific file context defined for Pages/Privacy.cshtml, navigating to /Privacy?culture=fr-FR loads a PO file such as:

msgid "Hello world!"
msgstr "Bonjour le monde!"

Changing the location of PO files

The default location of PO files can be changed in Programs.cs:

services.AddPortableObjectLocalization(options => options.ResourcesPath = "Localization");

In this example, the PO files are loaded from the Localization folder.

Implementing a custom logic for finding localization files

When more complex logic is needed to locate PO files, the OrchardCore.Localization.PortableObject.ILocalizationFileLocationProvider interface can be implemented and registered as a service. This is useful when PO files can be stored in varying locations or when the files have to be found within a hierarchy of folders.

Using a different default pluralized language

The package includes a Plural extension method that's specific to two plural forms. For languages requiring more plural forms, create an extension method. With an extension method, you won't need to provide any localization file for the default language — the original strings are already available directly in the code.

You can use the more generic Plural(int count, string[] pluralForms, params object[] arguments) overload which accepts a string array of translations.

By Sébastien Ros, Scott Addie and Hisham Bin Ateya

This article walks through the steps for using Portable Object (PO) files in an ASP.NET Core application with the Orchard Core framework.

Note: Orchard Core isn't a Microsoft product. Consequently, Microsoft provides no support for this feature.

View or download sample code (how to download)

What is a PO file?

PO files are distributed as text files containing the translated strings for a given language. Some advantages of using PO files instead of .resx files include:

  • PO files support pluralization; .resx files don't support pluralization.
  • PO files aren't compiled like .resx files. As such, specialized tooling and build steps aren't required.
  • PO files work well with collaborative online editing tools.

Example

Here is a sample PO file containing the translation for two strings in French, including one with its plural form:

fr.po

#: Pages/Index.cshtml:13
msgid "Hello world!"
msgstr "Bonjour le monde!"

msgid "There is one item."
msgid_plural "There are {0} items."
msgstr[0] "Il y a un élément."
msgstr[1] "Il y a {0} éléments."

This example uses the following syntax:

  • #:: A comment indicating the context of the string to be translated. The same string might be translated differently depending on where it's being used.
  • msgid: The untranslated string.
  • msgstr: The translated string.

In the case of pluralization support, more entries can be defined.

  • msgid_plural: The untranslated plural string.
  • msgstr[0]: The translated string for the case 0.
  • msgstr[N]: The translated string for the case N.

The PO file specification can be found here.

Configuring PO file support in ASP.NET Core

This example is based on an ASP.NET Core MVC application generated from a Visual Studio 2019 project template.

Referencing the package

Add a reference to the OrchardCore.Localization.Core NuGet package.

The .csproj file now contains a line similar to the following (version number may vary):

<PackageReference Include="OrchardCore.Localization.Core" Version="1.2.0" />

Registering the service

Add the required services to the ConfigureServices method of Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages()
        .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix);

    services.AddPortableObjectLocalization();

    services.Configure<RequestLocalizationOptions>(options => options
        .AddSupportedCultures("fr", "cs")
        .AddSupportedUICultures("fr", "cs")
    );
}

Add the required middleware to the Configure method of Startup.cs:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }

    app.UseRouting();
    app.UseStaticFiles();

    app.UseRequestLocalization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
    });
}

Add the following code to your Razor page of choice. Index.cshtml is used in this example.

@page
@using Microsoft.AspNetCore.Mvc.Localization
@inject IViewLocalizer Localizer
@{
    ViewData["Title"] = "Home";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

<p>@Localizer["Hello world!"]</p>

An IViewLocalizer instance is injected and used to translate the text "Hello world!".

Creating a PO file

Create a file named <culture code>.po in your application root folder. In this example, the file name is fr.po because the French language is used:

msgid "Hello world!"
msgstr "Bonjour le monde!"

This file stores both the string to translate and the French-translated string. Translations revert to their parent culture, if necessary. In this example, the fr.po file is used if the requested culture is fr-FR or fr-CA.

Testing the application

Run your application, and navigate to the URL /Index. The text Hello world! is displayed.

Navigate to the URL /Index?culture=fr-FR. The text Bonjour le monde! is displayed.

Pluralization

PO files support pluralization forms, which is useful when the same string needs to be translated differently based on a cardinality. This task is made complicated by the fact that each language defines custom rules to select which string to use based on the cardinality.

The Orchard Localization package provides an API to invoke these different plural forms automatically.

Creating pluralization PO files

Add the following content to the previously mentioned fr.po file:

msgid "There is one item."
msgid_plural "There are {0} items."
msgstr[0] "Il y a un élément."
msgstr[1] "Il y a {0} éléments."

See What is a PO file? for an explanation of what each entry in this example represents.

Adding a language using different pluralization forms

English and French strings were used in the previous example. English and French have only two pluralization forms and share the same form rules, which is that a cardinality of one is mapped to the first plural form. Any other cardinality is mapped to the second plural form.

Not all languages share the same rules. This is illustrated with the Czech language, which has three plural forms.

Create the cs.po file as follows, and note how the pluralization needs three different translations:

msgid "Hello world!"
msgstr "Ahoj světe!!"

msgid "There is one item."
msgid_plural "There are {0} items."
msgstr[0] "Existuje jedna položka."
msgstr[1] "Existují {0} položky."
msgstr[2] "Existuje {0} položek."

To accept Czech localizations, add "cs" to the list of supported cultures in the ConfigureServices method:

services.Configure<RequestLocalizationOptions>(options => options
                .AddSupportedCultures("fr", "cs")
                .AddSupportedUICultures("fr", "cs")
            );

Edit the Pages/Index.cshtml file to render localized, plural strings for several cardinalities:

<p>@Localizer.Plural(1, "There is one item.", "There are {0} items.")</p>
<p>@Localizer.Plural(2, "There is one item.", "There are {0} items.")</p>
<p>@Localizer.Plural(5, "There is one item.", "There are {0} items.")</p>

Note: In a real world scenario, a variable would be used to represent the count. Here, we repeat the same code with three different values to expose a very specific case.

Upon switching cultures, you see the following:

For /Index:

There is one item.
There are 2 items.
There are 5 items.

For /Index?culture=fr:

Il y a un élément.
Il y a 2 éléments.
Il y a 5 éléments.

For /Index?culture=cs:

Existuje jedna položka.
Existují 2 položky.
Existuje 5 položek.

Note that for the Czech culture, the three translations are different. The French and English cultures share the same construction for the two last translated strings.

Advanced tasks

Contextualizing strings

Applications often contain the strings to be translated in several places. The same string may have a different translation in certain locations within an app (Razor views or class files). A PO file supports the notion of a file context, which can be used to categorize the string being represented. Using a file context, a string can be translated differently, depending on the file context (or lack of a file context).

The PO localization services use the name of the full class or the view that's used when translating a string. This is accomplished by setting the value on the msgctxt entry.

Consider a minor addition to the previous fr.po example. A Razor view located at Pages/Index.cshtml can be defined as the file context by setting the reserved msgctxt entry's value:

msgctxt "Pages.Index"
msgid "Hello world!"
msgstr "Bonjour le monde!"

With the msgctxt set as such, text translation occurs when navigating to /Index?culture=fr-FR. The translation won't occur when navigating to /Privacy?culture=fr-FR.

When no specific entry is matched with a given file context, Orchard Core's fallback mechanism looks for an appropriate PO file without a context. Assuming there's no specific file context defined for Pages/Privacy.cshtml, navigating to /Privacy?culture=fr-FR loads a PO file such as:

msgid "Hello world!"
msgstr "Bonjour le monde!"

Changing the location of PO files

The default location of PO files can be changed in ConfigureServices:

services.AddPortableObjectLocalization(options => options.ResourcesPath = "Localization");

In this example, the PO files are loaded from the Localization folder.

Implementing a custom logic for finding localization files

When more complex logic is needed to locate PO files, the OrchardCore.Localization.PortableObject.ILocalizationFileLocationProvider interface can be implemented and registered as a service. This is useful when PO files can be stored in varying locations or when the files have to be found within a hierarchy of folders.

Using a different default pluralized language

The package includes a Plural extension method that's specific to two plural forms. For languages requiring more plural forms, create an extension method. With an extension method, you won't need to provide any localization file for the default language — the original strings are already available directly in the code.

You can use the more generic Plural(int count, string[] pluralForms, params object[] arguments) overload which accepts a string array of translations.

By Sébastien Ros, Scott Addie and Hisham Bin Ateya

This article walks through the steps for using Portable Object (PO) files in an ASP.NET Core application with the Orchard Core framework.

Note: Orchard Core isn't a Microsoft product. Consequently, Microsoft provides no support for this feature.

View or download sample code (how to download)

What is a PO file?

PO files are distributed as text files containing the translated strings for a given language. Some advantages of using PO files instead of .resx files include:

  • PO files support pluralization; .resx files don't support pluralization.
  • PO files aren't compiled like .resx files. As such, specialized tooling and build steps aren't required.
  • PO files work well with collaborative online editing tools.

Example

Here is a sample PO file containing the translation for two strings in French, including one with its plural form:

fr.po

#: Services/EmailService.cs:29
msgid "Enter a comma separated list of email addresses."
msgstr "Entrez une liste d'emails séparés par une virgule."

#: Views/Email.cshtml:112
msgid "The email address is \"{0}\"."
msgid_plural "The email addresses are \"{0}\"."
msgstr[0] "L'adresse email est \"{0}\"."
msgstr[1] "Les adresses email sont \"{0}\""

This example uses the following syntax:

  • #:: A comment indicating the context of the string to be translated. The same string might be translated differently depending on where it's being used.
  • msgid: The untranslated string.
  • msgstr: The translated string.

In the case of pluralization support, more entries can be defined.

  • msgid_plural: The untranslated plural string.
  • msgstr[0]: The translated string for the case 0.
  • msgstr[N]: The translated string for the case N.

The PO file specification can be found here.

Configuring PO file support in ASP.NET Core

This example is based on an ASP.NET Core MVC application generated from a Visual Studio 2017 project template.

Referencing the package

Add a reference to the OrchardCore.Localization.Core NuGet package.

The .csproj file now contains a line similar to the following (version number may vary):

<PackageReference Include="OrchardCore.Localization.Core" Version="1.0.0" />

Registering the service

Add the required services to the ConfigureServices method of Startup.cs:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc()
        .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix);

    services.AddPortableObjectLocalization();

    services.Configure<RequestLocalizationOptions>(options =>
        {
            var supportedCultures = new List<CultureInfo>
            {
                new CultureInfo("en-US"),
                new CultureInfo("en"),
                new CultureInfo("fr-FR"),
                new CultureInfo("fr")
            };

            options.DefaultRequestCulture = new RequestCulture("en-US");
            options.SupportedCultures = supportedCultures;
            options.SupportedUICultures = supportedCultures;
        });
}

Add the required middleware to the Configure method of Startup.cs:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }

    app.UseRouting();
    app.UseStaticFiles();

    app.UseRequestLocalization();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
    });
}

Add the following code to your Razor view of choice. About.cshtml is used in this example.

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

<p>@Localizer["Hello world!"]</p>

An IViewLocalizer instance is injected and used to translate the text "Hello world!".

Creating a PO file

Create a file named <culture code>.po in your application root folder. In this example, the file name is fr.po because the French language is used:

msgid "Hello world!"
msgstr "Bonjour le monde!"

This file stores both the string to translate and the French-translated string. Translations revert to their parent culture, if necessary. In this example, the fr.po file is used if the requested culture is fr-FR or fr-CA.

Testing the application

Run your application, and navigate to the URL /Home/About. The text Hello world! is displayed.

Navigate to the URL /Home/About?culture=fr-FR. The text Bonjour le monde! is displayed.

Pluralization

PO files support pluralization forms, which is useful when the same string needs to be translated differently based on a cardinality. This task is made complicated by the fact that each language defines custom rules to select which string to use based on the cardinality.

The Orchard Localization package provides an API to invoke these different plural forms automatically.

Creating pluralization PO files

Add the following content to the previously mentioned fr.po file:

msgid "There is one item."
msgid_plural "There are {0} items."
msgstr[0] "Il y a un élément."
msgstr[1] "Il y a {0} éléments."

See What is a PO file? for an explanation of what each entry in this example represents.

Adding a language using different pluralization forms

English and French strings were used in the previous example. English and French have only two pluralization forms and share the same form rules, which is that a cardinality of one is mapped to the first plural form. Any other cardinality is mapped to the second plural form.

Not all languages share the same rules. This is illustrated with the Czech language, which has three plural forms.

Create the cs.po file as follows, and note how the pluralization needs three different translations:

msgid "Hello world!"
msgstr "Ahoj světe!!"

msgid "There is one item."
msgid_plural "There are {0} items."
msgstr[0] "Existuje jedna položka."
msgstr[1] "Existují {0} položky."
msgstr[2] "Existuje {0} položek."

To accept Czech localizations, add "cs" to the list of supported cultures in the ConfigureServices method:

var supportedCultures = new List<CultureInfo>
{
    new CultureInfo("en-US"),
    new CultureInfo("en"),
    new CultureInfo("fr-FR"),
    new CultureInfo("fr"),
    new CultureInfo("cs")
};

Edit the Views/Home/About.cshtml file to render localized, plural strings for several cardinalities:

<p>@Localizer.Plural(1, "There is one item.", "There are {0} items.")</p>
<p>@Localizer.Plural(2, "There is one item.", "There are {0} items.")</p>
<p>@Localizer.Plural(5, "There is one item.", "There are {0} items.")</p>

Note: In a real world scenario, a variable would be used to represent the count. Here, we repeat the same code with three different values to expose a very specific case.

Upon switching cultures, you see the following:

For /Home/About:

There is one item.
There are 2 items.
There are 5 items.

For /Home/About?culture=fr:

Il y a un élément.
Il y a 2 éléments.
Il y a 5 éléments.

For /Home/About?culture=cs:

Existuje jedna položka.
Existují 2 položky.
Existuje 5 položek.

Note that for the Czech culture, the three translations are different. The French and English cultures share the same construction for the two last translated strings.

Advanced tasks

Contextualizing strings

Applications often contain the strings to be translated in several places. The same string may have a different translation in certain locations within an app (Razor views or class files). A PO file supports the notion of a file context, which can be used to categorize the string being represented. Using a file context, a string can be translated differently, depending on the file context (or lack of a file context).

The PO localization services use the name of the full class or the view that's used when translating a string. This is accomplished by setting the value on the msgctxt entry.

Consider a minor addition to the previous fr.po example. A Razor view located at Views/Home/About.cshtml can be defined as the file context by setting the reserved msgctxt entry's value:

msgctxt "Views.Home.About"
msgid "Hello world!"
msgstr "Bonjour le monde!"

With the msgctxt set as such, text translation occurs when navigating to /Home/About?culture=fr-FR. The translation won't occur when navigating to /Home/Contact?culture=fr-FR.

When no specific entry is matched with a given file context, Orchard Core's fallback mechanism looks for an appropriate PO file without a context. Assuming there's no specific file context defined for Views/Home/Contact.cshtml, navigating to /Home/Contact?culture=fr-FR loads a PO file such as:

msgid "Hello world!"
msgstr "Bonjour le monde!"

Changing the location of PO files

The default location of PO files can be changed in ConfigureServices:

services.AddPortableObjectLocalization(options => options.ResourcesPath = "Localization");

In this example, the PO files are loaded from the Localization folder.

Implementing a custom logic for finding localization files

When more complex logic is needed to locate PO files, the OrchardCore.Localization.PortableObject.ILocalizationFileLocationProvider interface can be implemented and registered as a service. This is useful when PO files can be stored in varying locations or when the files have to be found within a hierarchy of folders.

Using a different default pluralized language

The package includes a Plural extension method that's specific to two plural forms. For languages requiring more plural forms, create an extension method. With an extension method, you won't need to provide any localization file for the default language — the original strings are already available directly in the code.

You can use the more generic Plural(int count, string[] pluralForms, params object[] arguments) overload which accepts a string array of translations.