Локализация в .NET

Локализацией называется процесс преобразования ресурсов приложения в локализованные версии для конкретных языков и региональных параметров, которые приложение должно поддерживать. К этапу локализации следует приступать только после проверки локализуемости, которая позволяет убедиться в готовности глобализованного приложения к локализации.

Готовое к локализации приложение разделяется на два логических блока. Один из них содержит все элементы пользовательского интерфейса, а другой — исполняемый код. Блок пользовательского интерфейса содержит только локализуемые элементы, например строки, сообщения об ошибках, диалоговые окна, пункты меню, внедренные объектные ресурсы и т. д. для нейтрального языка и региональных параметров. Блок кода содержит только код приложения, который будет одинаковым для всех поддерживаемых языков и региональных параметров. Общеязыковая среда выполнения поддерживает модель ресурсов вспомогательной сборки, в которой исполняемый код приложения отделен от его ресурсов. Более подробную информацию о реализации этой модели см. в статье Ресурсы в .NET.

Для каждой локализованной версии приложения добавьте новую вспомогательную сборку, которая содержит локализованный блок пользовательского интерфейса, учитывающий все требования целевого языка и региональных параметров. Блок кода должен оставаться одинаковым для всех языков и региональных параметров. Объединение локализованной версии блока пользовательского интерфейса с блоком кода создает новую локализованную версию приложения.

В этой статье вы узнаете, как использовать реализации IStringLocalizer<T> и IStringLocalizerFactory. Во всех примерах исходного кода в этой статье используются пакеты NuGet Microsoft.Extensions.Localization и Microsoft.Extensions.Hosting. Дополнительные сведения о хостинге см. в разделе Универсальный узел .NET.

Файлы ресурсов

Основным механизмом изоляции локализуемых строк являются файлы ресурсов. Файл ресурсов — это XML-файл с расширением RESX. Файлы ресурсов преобразуются перед выполнением потребляемого приложения, другими словами, они представляют переведенное содержимое. Имя файла ресурсов, как правило, содержит идентификатор языкового стандарта и принимает следующую форму:

<FullTypeName><.Locale>.resx

Где:

  • Объект <FullTypeName> представляет локализуемые ресурсы для определенного типа.
  • Необязательный параметр <.Locale> представляет языковой стандарт содержимого файла ресурсов.

Указание языковых стандартов

Языковой стандарт должен определять язык, как минимум, но также может определять язык и региональные параметры (региональный язык), а также страну или регион. Обычно эти сегменты разделяются символом -. Ввиду особенностей языка и региональных параметров применяются правила "отката языка и региональных параметров", где более высокий приоритет имеют более подходящие варианты. Языковой стандарт должен сопоставляться с тегом известного языка. Дополнительные сведения см. в разделе CultureInfo.Name.

Сценарии отката языка и региональных параметров

Допустим, ваше локализованное приложение поддерживает различные языковые стандарты сербского языка и имеет следующие файлы ресурсов для MessageService:

Файлы Региональный язык Код страны
MessageService.sr-Cyrl-RS.resx (кириллица, Сербия) RS
MessageService.sr-Cyrl.resx Кириллица
MessageService.sr-Latn-BA.resx (латиница, Босния и Герцеговина) BA
MessageService.sr-Latn-ME.resx (латиница, Черногория) СООБ
MessageService.sr-Latn-RS.resx (латиница, Сербия) RS
MessageService.sr-Latn.resx Латинская
MessageService.sr.resx Латиница
MessageService.resx

† региональный язык по умолчанию для языка.

Когда приложение выполняется с параметром CultureInfo.CurrentCulture, для которого задан язык и региональные параметры "sr-Cyrl-RS", функция локализации пытается разрешить файлы в следующем порядке:

  1. MessageService.sr-Cyrl-RS.resx
  2. MessageService.sr-Cyrl.resx
  3. MessageService.sr.resx
  4. MessageService.resx

Но если ваше приложение было запущено с параметром CultureInfo.CurrentCulture, для которого задан язык и региональные параметры "sr-Latn-BA", функция локализации пытается разрешить файлы в следующем порядке:

  1. MessageService.sr-Latn-BA.resx
  2. MessageService.sr-Latn.resx
  3. MessageService.sr.resx
  4. MessageService.resx

Если подходящих совпадений не окажется, правило "отката языка и региональных параметров" проигнорирует языковые стандарты — это значит, что в отсутствие совпадения будет выбран файл ресурсов номер четыре. Если для языка и региональных параметров задано значение "fr-FR", функция локализации выберет файл MessageService.resx, который может создать проблемы. Дополнительные сведения см. в разделе Процесс использования резервных ресурсов.

Поиск ресурсов

Файлы ресурсов разрешаются автоматически в рамках подпрограммы поиска. Если имя вашего файла проекта отличается от корневого пространства имен проекта, имя сборки может быть другим. Это может препятствовать поиску ресурсов. Чтобы устранить это несоответствие, дайте подсказку службам локализации, используя RootNamespaceAttribute. Если этот атрибут указан, он используется при поиске ресурсов.

Пример проекта называется example.csproj, который создает example.dll и example.exe, однако Localization.Example используется пространство имен. Для исправления этого несоответствия примените атрибут уровня assembly:

[assembly: RootNamespace("Localization.Example")]

Регистрация служб локализации

Чтобы зарегистрировать службы локализации, во время настройки служб вызовите один из методов расширения AddLocalization. Это позволит включить внедрение зависимостей (DI) следующих типов:

Настройка параметров локализации

Перегрузка AddLocalization(IServiceCollection, Action<LocalizationOptions>) принимает параметр setupAction с типом Action<LocalizationOptions>. Это позволяет настроить параметры локализации.

HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);

builder.Services.AddLocalization(options =>
{
    options.ResourcesPath = "Resources";
});

// Omitted for brevity.

Файлы ресурсов могут находиться в любом месте проекта, но существуют и распространенные, проверенные приемы. Как правило, выбирается путь наименьшего сопротивления. В приведенном выше коде C#:

  • Создает построитель приложений по умолчанию.
  • Вызовы AddLocalization коллекции служб, указывая LocalizationOptions.ResourcesPath как "Resources".

Это вынуждает службы локализации выполнять поиск файлов ресурсов в каталоге Resources.

Использование IStringLocalizer<T> и IStringLocalizerFactory

После регистрациинастройки, если она выполняется) служб локализации с DI можно использовать следующие типы:

Для создания службы сообщений, способной возвращать локализованные строки, можно использовать 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;
    }
}

В приведенном выше коде C#:

  • Поле IStringLocalizer<MessageService> localizer объявлено.
  • Основной конструктор определяет IStringLocalizer<MessageService> параметр и записывает его в качестве аргумента localizer .
  • Метод GetGreetingMessage вызывает элемент IStringLocalizer.Item[String], передающий "GreetingMessage" в качестве аргумента.

IStringLocalizer также поддерживает параметризованные строковые ресурсы, которые следует учитывать в следующих 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;
    }
}

В приведенном выше коде C#:

  • Поле IStringLocalizer _localizer объявлено.
  • Основной конструктор принимает IStringLocalizerFactory параметр, который используется для создания IStringLocalizer из ParameterizedMessageService типа и назначает его полю _localizer .
  • Метод GetFormattedMessage вызывает IStringLocalizer.Item[String, Object[]], передает "DinnerPriceFormat", объект dateTime и dinnerPrice в качестве аргументов.

Внимание

IStringLocalizerFactory не требуется. Лучше, если службы-адресаты будут требовать IStringLocalizer<T>.

Оба индексатора IStringLocalizer.Item[] возвращают LocalizedString с неявными преобразованиями в string?.

Сборка

Чтобы проиллюстрировать приложение, используя обе службы сообщений, а также файлы локализации и ресурсов, изучите следующий файл Program.cs:

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();

В приведенном выше коде C#:

  • RootNamespaceAttribute определяет "Localization.Example" как корневое пространство имен.
  • Console.OutputEncoding назначается Encoding.Unicode.
  • При передаче одного аргумента в args свойствам CultureInfo.CurrentCulture и CultureInfo.CurrentUICulture присваивается результат CultureInfo.GetCultureInfo(String), который получает значение arg[0].
  • Host создается с использованием значений по умолчанию.
  • Службы локализации, MessageService и ParameterizedMessageService, регистрируются в IServiceCollection для внедрения зависимостей.
  • Для устранения шума ведение журнала настраивается на игнорирование любого уровня журнала ниже предупреждения.
  • Объект MessageService разрешается из экземпляра IServiceProvider, а результирующее сообщение записывается в журнал.
  • Объект ParameterizedMessageService разрешается из экземпляра IServiceProvider, а результирующее отформатированное сообщение записывается в журнал.

Каждый из классов *MessageService определяет набор RESX-файлов, каждый из которых имеет одну запись. Ниже приведен пример содержимого файлов ресурсов MessageService, начиная с 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>

Ниже приведен пример содержимого файлов ресурсов ParameterizedMessageService, начиная с 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>

Совет

Все XML-комментарии, схемы и элементы <resheader> в файле ресурсов намеренно опущены для краткости.

Примеры выполнения

В следующем примере выполняется отображение различных локализованных выходных данных с заданными языковыми стандартами.

Возьмем, например, "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 ¤.

При пропуске аргумента в интерфейсе командной строки .NET для запуска проекта используется язык и региональные параметры системы по умолчанию. В этом случае "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.

При передаче "sr-Cryl-RS" система находит правильные файлы ресурсов и применяется локализация:

dotnet run --project .\example\example.csproj sr-Cryl-RS

warn: Localization.Example[0]
      Здраво пријатељи, ".NЕТ" девелопер заједница је узбуђена што вас види овде!
warn: Localization.Example[0]
      У уторак, 03. август 2021. моја вечера је коштала 38 RSD.

Пример приложения не включает файлы ресурсов для "fr-CA", но при вызове с этим языком и региональными параметрами используются нелокализованные файлы ресурсов.

Предупреждение

Поскольку язык и региональные параметры найдены, а верные файлы ресурсов отсутствуют, при применении форматирования выполняется частичная локализация:

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

См. также