Localization in .NET
Localization is the process of translating an application's resources into localized versions for each culture that the application will support. You should proceed to the localization step only after completing the Localizability review step to verify that the globalized application is ready for localization.
An application that is ready for localization is separated into two conceptual blocks: a block that contains all user interface elements and a block that contains executable code. The user interface block contains only localizable user-interface elements such as strings, error messages, dialog boxes, menus, embedded object resources, and so on for the neutral culture. The code block contains only the application code to be used by all supported cultures. The common language runtime supports a satellite assembly resource model that separates an application's executable code from its resources. For more information about implementing this model, see Resources in .NET.
For each localized version of your application, add a new satellite assembly that contains the localized user interface block translated into the appropriate language for the target culture. The code block for all cultures should remain the same. The combination of a localized version of the user interface block with the code block produces a localized version of your application.
In this article, you will learn how to use the IStringLocalizer<T> and IStringLocalizerFactory implementations. All of the example source code in this article relies on the Microsoft.Extensions.Localization
and Microsoft.Extensions.Hosting
NuGet packages. For more information on hosting, see .NET Generic Host.
Resource files
The primary mechanism for isolating localizable strings is with resource files. A resource file is an XML file with the .resx file extension. Resource files are translated prior to the execution of the consuming application—in other words, they represent translated content at rest. A resource file name most commonly contains a locale identifier, and takes on the following form:
<FullTypeName><.Locale>.resx
Where:
- The
<FullTypeName>
represents localizable resources for a specific type. - The optional
<.Locale>
represents the locale of the resource file contents.
Specifying locales
The locale should define the language, at a bare minimum, but it can also define the culture (regional language), and even the country or region. These segments are commonly delimited by the -
character. With the added specificity of a culture, the "culture fallback" rules are applied where best matches are prioritized. The locale should map to a well-known language tag. For more information, see CultureInfo.Name.
Culture fallback scenarios
Imagine that your localized app supports various Serbian locales, and has the following resource files for its MessageService
:
File | Regional language | Country Code |
---|---|---|
MessageService.sr-Cyrl-RS.resx | (Cyrillic, Serbia) | RS |
MessageService.sr-Cyrl.resx | Cyrillic | |
MessageService.sr-Latn-BA.resx | (Latin, Bosnia & Herzegovina) | BA |
MessageService.sr-Latn-ME.resx | (Latin, Montenegro) | ME |
MessageService.sr-Latn-RS.resx | (Latin, Serbia) | RS |
MessageService.sr-Latn.resx | Latin | |
MessageService.sr.resx | † Latin | |
MessageService.resx |
† The default regional language for the language.
When your app is running with the CultureInfo.CurrentCulture set to a culture of "sr-Cyrl-RS"
localization attempts to resolve files in the following order:
- MessageService.sr-Cyrl-RS.resx
- MessageService.sr-Cyrl.resx
- MessageService.sr.resx
- MessageService.resx
However, if your app was running with the CultureInfo.CurrentCulture set to a culture of "sr-Latn-BA"
localization attempts to resolve files in the following order:
- MessageService.sr-Latn-BA.resx
- MessageService.sr-Latn.resx
- MessageService.sr.resx
- MessageService.resx
The "culture fallback" rule will ignore locales when there are no corresponding matches, meaning resource file number four is selected if it's unable to find a match. If the culture was set to "fr-FR"
, localization would end up falling to the MessageService.resx file which can be problematic. For more information, see The resource fallback process.
Resource lookup
Resource files are automatically resolved as part of a lookup routine. If your project file name is different than the root namespace of your project, the assembly name might differ. This can prevent resource lookup from being otherwise successful. To address this mismatch, use the RootNamespaceAttribute to provide a hint to the localization services. When provided, it is used during resource lookup.
The example project is named example.csproj, which creates an example.dll and example.exe—however, the Localization.Example
namespace is used. Apply an assembly
level attribute to correct this mismatch:
[assembly: RootNamespace("Localization.Example")]
Register localization services
To register localization services, call one of the AddLocalization extension methods during the configuration of services. This will enable dependency injection (DI) of the following types:
- Microsoft.Extensions.Localization.IStringLocalizer<T>
- Microsoft.Extensions.Localization.IStringLocalizerFactory
Configure localization options
The AddLocalization(IServiceCollection, Action<LocalizationOptions>) overload accepts a setupAction
parameter of type Action<LocalizationOptions>
. This allows you to configure localization options.
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddLocalization(options =>
{
options.ResourcesPath = "Resources";
});
// Omitted for brevity.
Resource files can live anywhere in a project, but there are common practices in place that have proven to be successful. More often than not, the path of least resistance is followed. The preceding C# code:
- Creates the default host app builder.
- Calls
AddLocalization
on the service collection, specifying LocalizationOptions.ResourcesPath as"Resources"
.
This would cause the localization services to look in the Resources directory for resource files.
Use IStringLocalizer<T>
and IStringLocalizerFactory
After you've registered (and optionally configured) the localization services, you can use the following types with DI:
To create a message service that is capable of returning localized strings, consider the following MessageService
:
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Localization;
namespace Localization.Example;
public sealed class MessageService(IStringLocalizer<MessageService> localizer)
{
[return: NotNullIfNotNull(nameof(localizer))]
public string? GetGreetingMessage()
{
LocalizedString localizedString = localizer["GreetingMessage"];
return localizedString;
}
}
In the preceding C# code:
- A
IStringLocalizer<MessageService> localizer
field is declared. - The primary constructor defines an
IStringLocalizer<MessageService>
parameter and captures it as alocalizer
argument. - The
GetGreetingMessage
method invokes the IStringLocalizer.Item[String] passing"GreetingMessage"
as an argument.
The IStringLocalizer
also supports parameterized string resources, consider the following ParameterizedMessageService
:
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Localization;
namespace Localization.Example;
public class ParameterizedMessageService(IStringLocalizerFactory factory)
{
private readonly IStringLocalizer _localizer =
factory.Create(typeof(ParameterizedMessageService));
[return: NotNullIfNotNull(nameof(_localizer))]
public string? GetFormattedMessage(DateTime dateTime, double dinnerPrice)
{
LocalizedString localizedString = _localizer["DinnerPriceFormat", dateTime, dinnerPrice];
return localizedString;
}
}
In the preceding C# code:
- A
IStringLocalizer _localizer
field is declared. - The primary constructor takes an
IStringLocalizerFactory
parameter, which is used to create anIStringLocalizer
from theParameterizedMessageService
type, and assigns it to the_localizer
field. - The
GetFormattedMessage
method invokes IStringLocalizer.Item[String, Object[]], passing"DinnerPriceFormat"
, adateTime
object, anddinnerPrice
as arguments.
Important
The IStringLocalizerFactory
isn't required. Instead, it is preferred for consuming services to require the IStringLocalizer<T>.
Both IStringLocalizer.Item[] indexers return a LocalizedString, which have implicit conversions to string?
.
Put it all together
To exemplify an app using both message services, along with localization and resource files, consider the following Program.cs file:
using System.Globalization;
using Localization.Example;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Localization;
using Microsoft.Extensions.Logging;
using static System.Console;
using static System.Text.Encoding;
[assembly: RootNamespace("Localization.Example")]
OutputEncoding = Unicode;
if (args is [var cultureName])
{
CultureInfo.CurrentCulture =
CultureInfo.CurrentUICulture =
CultureInfo.GetCultureInfo(cultureName);
}
HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
builder.Services.AddLocalization();
builder.Services.AddTransient<MessageService>();
builder.Services.AddTransient<ParameterizedMessageService>();
builder.Logging.SetMinimumLevel(LogLevel.Warning);
using IHost host = builder.Build();
IServiceProvider services = host.Services;
ILogger logger =
services.GetRequiredService<ILoggerFactory>()
.CreateLogger("Localization.Example");
MessageService messageService =
services.GetRequiredService<MessageService>();
logger.LogWarning(
"{Msg}",
messageService.GetGreetingMessage());
ParameterizedMessageService parameterizedMessageService =
services.GetRequiredService<ParameterizedMessageService>();
logger.LogWarning(
"{Msg}",
parameterizedMessageService.GetFormattedMessage(
DateTime.Today.AddDays(-3), 37.63));
await host.RunAsync();
In the preceding C# code:
- The RootNamespaceAttribute sets
"Localization.Example"
as the root namespace. - The Console.OutputEncoding is assigned to Encoding.Unicode.
- When a single argument is passed to
args
, the CultureInfo.CurrentCulture and CultureInfo.CurrentUICulture are assigned the result of CultureInfo.GetCultureInfo(String) given thearg[0]
. - The Host is created with defaults.
- The localization services,
MessageService
, andParameterizedMessageService
are registered to theIServiceCollection
for DI. - To remove noise, logging is configured to ignore any log level lower than a warning.
- The
MessageService
is resolved from theIServiceProvider
instance and its resulting message is logged. - The
ParameterizedMessageService
is resolved from theIServiceProvider
instance and its resulting formatted message is logged.
Each of the *MessageService
classes defines a set of .resx files, each with a single entry. Here is the example content for the MessageService
resource files, starting with MessageService.resx:
<?xml version="1.0" encoding="utf-8"?>
<root>
<data name="GreetingMessage" xml:space="preserve">
<value>Hi friends, the ".NET" developer community is excited to see you here!</value>
</data>
</root>
MessageService.sr-Cyrl-RS.resx:
<?xml version="1.0" encoding="utf-8"?>
<root>
<data name="GreetingMessage" xml:space="preserve">
<value>Здраво пријатељи, ".NЕТ" девелопер заједница је узбуђена што вас види овде!</value>
</data>
</root>
MessageService.sr-Latn.resx:
<?xml version="1.0" encoding="utf-8"?>
<root>
<data name="GreetingMessage" xml:space="preserve">
<value>Zdravo prijatelji, ".NET" developer zajednica je uzbuđena što vas vidi ovde!</value>
</data>
</root>
Here is the example content for the ParameterizedMessageService
resource files, starting with ParameterizedMessageService.resx:
<?xml version="1.0" encoding="utf-8"?>
<root>
<data name="DinnerPriceFormat" xml:space="preserve">
<value>On {0:D} my dinner cost {1:C}.</value>
</data>
</root>
ParameterizedMessageService.sr-Cyrl-RS.resx:
<?xml version="1.0" encoding="utf-8"?>
<root>
<data name="DinnerPriceFormat" xml:space="preserve">
<value>У {0:D} моја вечера је коштала {1:C}.</value>
</data>
</root>
ParameterizedMessageService.sr-Latn.resx:
<?xml version="1.0" encoding="utf-8"?>
<root>
<data name="DinnerPriceFormat" xml:space="preserve">
<value>U {0:D} moja večera je koštala {1:C}.</value>
</data>
</root>
Tip
All of the resource file XML comments, schema, and <resheader>
elements are intentionally omitted for brevity.
Example runs
The following example runs show the various localized outputs, given targeted locales.
Consider "sr-Latn"
:
dotnet run --project .\example\example.csproj sr-Latn
warn: Localization.Example[0]
Zdravo prijatelji, ".NET" developer zajednica je uzbuđena što vas vidi ovde!
warn: Localization.Example[0]
U utorak, 03. avgust 2021. moja večera je koštala 37,63 ¤.
When omitting an argument to the .NET CLI to run the project, the default system culture is used—in this case "en-US"
:
dotnet run --project .\example\example.csproj
warn: Localization.Example[0]
Hi friends, the ".NET" developer community is excited to see you here!
warn: Localization.Example[0]
On Tuesday, August 3, 2021 my dinner cost $37.63.
When passing "sr-Cryl-RS"
, the correct corresponding resource files are found and the localization applied:
dotnet run --project .\example\example.csproj sr-Cryl-RS
warn: Localization.Example[0]
Здраво пријатељи, ".NЕТ" девелопер заједница је узбуђена што вас види овде!
warn: Localization.Example[0]
У уторак, 03. август 2021. моја вечера је коштала 38 RSD.
The sample application does not provide resource files for "fr-CA"
, but when called with that culture, the non-localized resource files are used.
Warning
Since the culture is found but the correct resource files are not, when formatting is applied you end up with partial localization:
dotnet run --project .\example\example.csproj fr-CA
warn: Localization.Example[0]
Hi friends, the ".NET" developer community is excited to see you here!
warn: Localization.Example[0]
On mardi 3 août 2021 my dinner cost 37,63 $.