Skip to content

Instantly share code, notes, and snippets.

@sebastienros
Last active September 27, 2023 01:01
Show Gist options
  • Save sebastienros/7980aad2ab33dd3a1c011fcf5a3f350c to your computer and use it in GitHub Desktop.
Save sebastienros/7980aad2ab33dd3a1c011fcf5a3f350c to your computer and use it in GitHub Desktop.
Configuring Portable Object localization in ASP.NET Core

Configuring Portable Object localization in ASP.NET Core

The package which is used in this article is avaible on MyGet on this url: https://www.myget.org/F/orchardcore-preview/api/v3/index.json

This article walks you through the steps for using Portable Object files (PO files) inside your ASP.NET Core application.

What is a PO file

PO files are files that contain the translated strings for a given language. They reveal very useful as contrary to standard resx files, PO files support pluralization and are distributed as text files.

Example

Here is a sample PO file that contains 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 used for editors to understand what the context of the string to translate is, as the same string might be translated differently depending on where it is being used.
  • msgid: The untranslated string.
  • msgstr: The translated string.

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

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

The specification for PO files can be found on this site: https://www.gnu.org/savannah-checkouts/gnu/gettext/manual/html_node/PO-Files.html

Configuring PO file support in ASP.NET Core

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

Referencing the package

Add a reference to the Nuget package named OrchardCore.Localization.Core.

The project file should contain a new line similar to this:

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

Registering the service

In your Startup class add the required services:

Startup.cs

...
public void ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddMvc();

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

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

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    ...
    app.UseRequestLocalization();
    ...
}
...

Edit the file About.cshtml as is:

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

@{
    ViewData["Title"] = "About";
}

<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<p>Use this area to provide additional information.</p>

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

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

Next step is about creating a PO file which will contain the translations in French.

Creating a PO file

Create the file named fr.po in your application root folder with this content:

fr.po

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

This file only contains the string to translate and the translated string. Translations will fallback to their parent culture if necessary. In this example if the requested culture is fr-FR or fr-CA the fr.po file will still be used.

Testing the application

Run your application an navigate to the url /Home/About. The text Hello world! should be displayed.

Navigate to the url /Home/About?culture=fr-FR. The text Bonjour le monde! should be 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

If the previous fr.po file, add the following content:

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."

Adding a language using different pluralization forms

In the previous example we used English and French strings. English and French have only two pluralization forms and share the same form rules, which is that a cardinality of 1 is mapped to the first plural form, and any other cardinality is mapped to the second plural form.

Unfortunately all the languages don't share the same rules. As an example we will use the Czech language as it has three plural forms.

Create the cs.po file as is, and not 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."

Add "cs" to the list of supported cultures in Startup.cs to accept Czech localizations:

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

Edit the view Views/Home/About.cshtml 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>

In a real world scenario you would use a variable to represent the count variable. Here we repeat the same code with three different values to expose a very specific case.

Now by switching cultures you should see:

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 tranlations are different, while the French and English one have the same construction for the two last translated strings.

Advanced tasks

Contextualizing strings

Often applications contain the same strings to be translated in several places while requiring the flexibility to define different translations. A PO files supports the notion of context that can be used to categorize the string that is represented.

The Portable Object localization services can use the name of the full class name or view name that is used when translating a string. This is done by setting the value on the msgctx entry.

Taking the previous example, the entry could have been written as is: fr.po

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

When no context is specified, it will be used as a fallback for any string whose context doesn't match any specific one.

Changing the location of PO files

You can change the default location of PO files in the application by setting a base folder name.

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

In this example the PO files will be loaded from the Localization folder.

Implementing a custom logic for finding localization files

In case some more complex logic is needed to locate PO files, the interface OrchardCore.Localization.PortableObject.ILocalizationFileLocationProvider can be implemented and registered as a service. For instance if PO files can be in a difference locations, or have to be found in a hierarchy of folders.

Using a different default pluralized language

The package includes a Plural extension method that is specific to two plural forms as this is the most common. If your main language is different and requires more plural forms for instance, you should create your own extension method. This way you won't need to provide any localization file for the default language as all the original strings will already be available directly in the code.

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

@raphaelaubin
Copy link

How to localize DataAnnotation?

`using System;
using System.ComponentModel.DataAnnotations;

namespace WebBaseProject.ViewModels {
public class UserViewModel
{
public UserViewModel(String pName) {
Name = pName;
}

    [Display(Name="MsgId")]
    [Required(ErrorMessage = "MsgId")]
    public String Name { get; set; }
}

}
`

@Tbd19
Copy link

Tbd19 commented Jul 16, 2018

I have defined a custom validation like
public sealed class UserEmailRequiredAttribute : ValidationAttribute, IClientModelValidator
add use the ErrorMessageString from the ValidationAttribute but it is not translated.

Surprisingly if I use Required or EmailAddress attributes the translation is done.

Is there a special thing that I have to do for this to work?

@satakane
Copy link

satakane commented Oct 4, 2018

I was configuring the PO file support step by step and got an exception. Adding "AddViewLocalization" seemed to have fixed it.

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1)
		.AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix);

Solution found from here:
https://stackoverflow.com/questions/40286575/no-service-for-type-microsoft-aspnetcore-mvc-localization-ihtmllocalizer-has-b

Also, there is a typo. The correct abbreviation for context is "msgctxt" not "msgctx". So correct format would be:

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

@nyghtrocker
Copy link

Hi, Is it possible to replace po files location in orchard core cms using nuget packages.? In Statrp.cs I have
services.AddOrchardCms();
services.AddSingleton<ILocalizationFileLocationProvider, MyLIPoFileLocationProvider>(); and
public class MyLIPoFileLocationProvider: ILocalizationFileLocationProvider
{
private readonly string _resourcesContainer;

    public MyLIPoFileLocationProvider(IOptions<LocalizationOptions> localizationOptions)
    {
        _resourcesContainer = localizationOptions.Value.ResourcesPath;
    }

    public IEnumerable<IFileInfo> GetLocations(string cultureName)
    {

        yield return new PhysicalFileInfo(new FileInfo("Localization"));
    }

}

The GetLocations method never hit the breakpoint?

@jdescelliers
Copy link

What about using gettext msgids in Selenium tests? To verify that the page has a given text for instance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment