Localization (OrchardCore.Localization)¶
This module provides the infrastructure necessary to support the PO (Portable Object) localization file format.
It also supports plural forms.
Online translations¶
The localization files for the different cultures are available on Crowdin.
PO files locations¶
PO files are found at these locations:
- For each module and theme all files matching
[ModuleLocation]/Localization/[CultureName].po - All files matching
/Localization/[CultureName].po - For each tenant all files matching
/App_Data/Sites/[TenantName]/Localization/[CultureName].po - For each module and theme all files matching
/Localization/[ModuleId]/[CultureName].po/Localization/[ModuleId]-[CultureName].po/Localization/[CultureName]/[ModuleId].po
[CultureName] can be either the culture neutral part, e.g. fr, or the full one, e.g. fr-CA.
It is suggested to put your localization files in the /Localization/ folder if you are using Docker.
Especially if mounting a volume at /App_Data/, as mounting hides pre-existing files.
Note
If you edit a .po file, you need to restart the application to make your change effective.
Publishing Localization files¶
The PO files need to be included in the publish output directory.
Add the following configurations to your [Web Project].csproj file to include them as Content.
<ItemGroup>
<Content Include="Localization\**" >
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
Note
Translation files can be provided by a module, in that case they are embedded automatically in the module assembly unless Visual Studio added some bad item groups in the project file.
Recipe Step¶
Cultures can be added during recipes using the settings step. Here is a sample step:
{
"name": "settings",
"LocalizationSettings": {
"DefaultCulture": "fr",
"SupportedCultures": [ "fr", "en" ]
}
}
Examples¶
/Localization/fr.po/Localization/fr-CA.po/Localization/es-MX.po
File format¶
This article explains how PO files are organized, including plural forms.
https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html
Translation contexts¶
To prevent entries in different PO files from overriding each other, they define a context for each translation string.
For instance two views could use the string named Hello but they might have different translation. It's then necessary to
provide two entries and specify which context is associated with each translation. In this case each view name is a context.
From a View¶
The context string must match the view location up to the module folder.
View¶
Assuming the view's path is TheAdmin\Views\Layout.cshtml.
PO File¶
msgctxt "TheAdmin.Views.Layout"
msgid "Hello"
msgstr "Bonjour"
From a Service¶
The context string must match the full name of the type the localizer is injecting in.
Source¶
namespace MyNamespace
{
public class MyService : IMyService
{
private readonly IStringLocalizer S;
public MyService(IStringLocalizer<MyService> localizer)
{
S = localizer;
}
public void DoSomething()
{
Console.WriteLine(S["Hello"]);
}
}
}
PO file¶
msgctxt "MyNamespace.MyService"
msgid "Hello"
msgstr "Bonjour"
Deferring localization with LocalizationSource¶
When no localizer is available where a string is declared, use LocalizationSource from OrchardCore.Abstractions to keep the untranslated value and the source type of its localizer:
using OrchardCore.Localization;
var description = LocalizationSource.Create("Manage widgets", typeof(MyPermissions));
Prefer LocalizationSource.Create<T>(value) when the source type can be a generic type argument. Use LocalizationSource.Create(value, type) for a runtime source type or a static class, which cannot be a generic type argument. Both methods preserve the original value and type without translating or formatting them. Prefer these explicit factory calls over constructor syntax so source-extraction tools can identify the declaration without inferring the type of new(...).
Value is the original text, used as the PO msgid. Type supplies the localizer's context, just as with IStringLocalizer<MyPermissions> or IHtmlLocalizer<MyPermissions>. For example:
msgctxt "MyModule.MyPermissions"
msgid "Manage widgets"
msgstr "Gérer les widgets"
LocalizationSource is a sealed record with get-only properties. It does not translate, format, or encode the value, so a static readonly source can safely be reused across instances, tenants, and cultures. Resolve the translation at display time, using the current tenant and culture's factory:
using Microsoft.AspNetCore.Mvc.Localization;
using Microsoft.Extensions.Localization;
LocalizedString text = stringLocalizerFactory.Localize(description);
LocalizedHtmlString html = htmlLocalizerFactory.Localize(description);
Both extensions call factory.Create(source.Type) and look up source.Value. The same source can be rendered as text or HTML; it does not commit to an encoding policy at declaration time. A string localizer returns a LocalizedString that Razor HTML encodes when rendered. An HTML localizer returns a LocalizedHtmlString that preserves markup in the translation and HTML encodes its format arguments.
Pass format arguments to Localize, not to LocalizationSource.Create, so that the original key is preserved and the translated template is formatted:
var welcome = LocalizationSource.Create<MyMessages>("<strong>Welcome {0}</strong>");
var translated = htmlLocalizerFactory.Localize(welcome, userName);
With string localization, no formatting is applied when no arguments are supplied. HTML localization follows LocalizedHtmlString's composite formatting rules when rendered: supply arguments for placeholders and escape literal braces as {{ and }}. A source constructed without a type, for example LocalizationSource.Create("Hello"), has no PO context: the factory extensions return the original value, with any supplied arguments applied. A null source produces a null result.
Warning
Use HTML localization only for trusted translation markup. It does not encode the translation itself. For plain labels, permission descriptions, attributes, and other text, use string localization and retain the rendering layer's encoding.
Pluralization¶
This module also provides support for pluralization.
It is necessary to reference the OrchardCore.Localization.Abstractions package in order to be able to use it.
Sample PO file¶
msgctxt "TheAdmin.Views.Layout"
msgid "{0} book"
msgid_plural "{0} books"
msgstr[0] "[{0} livre]"
msgstr[1] "[{0} livres]"
Usage¶
- Import the
using Microsoft.Extensions.Localizationnamespace. - Inject an instance of
IStringLocalizerorIViewLocalizer(represented as theTvariable in the following example).
T.Plural(count, "{0} book", "{0} books")
"{0} book" is the singular form
* "{0} books" is the plural form
* count will determine if the singular or plural form is used and will replace the {0} placeholder
Warning
You should not hardcode a number in the singular or plural forms because different languages have different rules about when each form is used.
Pluralizing a deferred LocalizationSource¶
IStringLocalizerFactory and IHtmlLocalizerFactory also expose a Plural extension for a LocalizationSource declared without an available localizer, for example a static field evaluated before a tenant or culture is known. It requires OrchardCore.Localization.Abstractions and lives in the same namespaces as the plural helpers above (Microsoft.Extensions.Localization / Microsoft.AspNetCore.Mvc.Localization):
using Microsoft.Extensions.Localization;
using OrchardCore.Localization;
private static readonly LocalizationSource _item = LocalizationSource.Create<MyMessages>("{0} item for {1}");
var result = stringLocalizerFactory.Plural(count, _item, "{0} items for {1}", ownerName);
The source's Value is the singular PO msgid, exactly like Localize. count is always inserted automatically as the first format argument ({0}); any additional arguments you pass, such as ownerName, follow it ({1}, {2}, ...). The HTML factory extension has the same signature and semantics, deferring HTML encoding of the arguments to render time, like Localize.
For a context-bearing source (Type set), the extension resolves factory.Create(source.Type) and delegates to the existing IStringLocalizer.Plural/IHtmlLocalizer.Plural extension, so the culture's PO PluralRule always decides the translated form, including languages with a dedicated zero form or more than two plural forms. For a context-free source (Type null, no PO lookup possible), the extension falls back to the English rule — singular only when count == 1 — without resolving a factory, matching the existing context-free behavior of Localize. A null source returns null without resolving a factory, same as Localize.
Extract translations to PO files¶
In order to generate the .po files, you can use this tool.
Note
LocalizationSource.Create(...) and LocalizationSource.Create<T>(...) provide explicit declarations for source-extraction tooling. Support for recognizing these calls depends on the external extractor; Orchard Core does not modify that tool. Runtime PO keys and contexts are unchanged.
The simpler way to use it is to install it with this command:
dotnet tool install --global OrchardCoreContrib.PoExtractor
Then, you will be able to run this command to generate the .po files:
extractpo <INTPUT_PATH> <OUTPUT_PATH> [-l|--language {"C#"|"VB"}] [-t|--template {"razor"|"liquid"}]
JavaScript Localization¶
See JavaScript Localization (IJSLocalizer) for guidance on exposing PO-file-backed translations to JavaScript / TypeScript assets.
Liquid filters¶
For more information on using locaization filters in Liquid templates, see the Liquid Localization filters documentation.
Video¶
Recipe Configuration¶
Localization settings can be configured using the Settings recipe step:
{
"steps": [
{
"name": "settings",
"LocalizationSettings": {
"DefaultCulture": "en-US",
"SupportedCultures": [
"en-US",
"fr-FR",
"es-ES"
],
"FallBackToParentCulture": true
}
}
]
}
| Property | Type | Description |
|---|---|---|
DefaultCulture |
String | The default culture for the site (e.g., en-US). |
SupportedCultures |
Array of String | The list of supported cultures. |
FallBackToParentCulture |
Boolean | Whether to fall back to the parent culture when a translation is not found. |