Глобализация и локализация

Rick Anderson, Damien Bowden, Bart Calixto, Nadeem Afana

Создание мультиязычного веб сайта с помощью ASP.NET Core позволит вам привлечь на сайт больше пользователей. ASP.NET Core предлагает сервисы и связующее ПО для локализации различных языков и культур.

Интернационализация включает в себя глобализацию и локализацию. Глобализация - это процесс при создании приложений, направленный на поддержку различных культур. Глобализация добавляет поддержку ввода, отображения и вывода определенного набора языковых скриптов, имеющих отношение к конкретным языковым областям.

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

Локализация приложения включает в себя следующее:

  1. Сделать контекст приложения локализуемым
  2. Предоставить локализованные ресурсы для языков и культур, которые вы поддерживаете
  3. Реализовать стратегию выбора языка/культуры для каждого запроса

Как сделать контекст приложения локализуемым

Введенные в ASP.NET Core :dn:iface:`~Microsoft.Extensions.Localization.IStringLocalizer` и :dn:iface:`~Microsoft.Extensions.Localization.IStringLocalizer\<T>` были созданы для улучшения производительности при разработке локализованных приложений. IStringLocalizer использует ResourceManager и ResourceReader, чтобы предоставить необходимые ресурсы (конкретные для определенной культуры) при рантайме. У простого интерфейса есть индексатор и IEnumerable для возвращения локализованных строк. IStringLocalizer не требует, чтобы вы хранили в исходном файле строки на языке по умолчанию. Вы можете разрабатывать приложение, направленное на локализацию, и вам не нужно создавать исходные файлы на ранней стадии разработки. В следующем коде показано, как как подготовить строку “About Title” для локализации.

using Microsoft.AspNet.Mvc;
using Microsoft.Extensions.Localization;

namespace Localization.StarterWeb.Controllers
{
    [Route("api/[controller]")]
    public class AboutController : Controller
    {
        private readonly IStringLocalizer<AboutController> _localizer;

        public AboutController(IStringLocalizer<AboutController> localizer)
        {
            _localizer = localizer;
        }

        [HttpGet]
        public string Get()
        {
            return _localizer["About Title"];
        }
    }
}

В этом примере реализация IStringLocalizer<T> происходит от Внедрение зависимостей (Dependency Injection). Если локализованное значение “About Title” не найдено, тогда возвращается ключ индексатора, то есть, строка “About Title”. Вы можете оставлять в приложении строки на языке по умолчанию, подготовить их для локализации, а сами сосредоточиться на разработке приложения. Вы разрабатываете приложение на языке по умолчанию и подготавливаете его для локализации без необходимости создания исходного файла по умолчанию. Как вариант, вы можете использовать традиционный подход и предоставить ключ для получения строки на языке по умолчанию. Для многих разработчиков новый подход, когда файла .resx нет и когда нужно просто “обернуть” строковые литералы, может убрать переизбыток кода для локализации в приложении. Другие разработчики все же предпочитают традиционный подход, поскольку в таком случае упрощается работа с длинными строковыми литералами и локализованные строки легче обновлять.

Используйте реализацию IHtmlLocalizer<T> для ресурсов, которые содержат HTML. IHtmlLocalizer HTML шифрует аргументы, которые форматированы в исходную строку, а не исходную строку. В примере ниже только значение параметра name шифровано в HTML.

using System;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Localization;
using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.Localization;

namespace Localization.StarterWeb.Controllers
{
    public class BookController : Controller
    {
        private readonly IHtmlLocalizer<BookController> _localizer;

        public BookController(IHtmlLocalizer<BookController> localizer)
        {
            _localizer = localizer;
        }

        public IActionResult Hello(string name)
        {
            ViewData["Message"] = _localizer["<b>Hello</b><i> {0}</i>", name];

            return View();
        }
Note:Как правило, мы локализуем текст, а не HTML.

На самом низком уровне вы можете получить из Внедрение зависимостей (Dependency Injection) IStringLocalizerFactory:

 public class TestController : Controller
 {
     private readonly IStringLocalizer _localizer;
     private readonly IStringLocalizer _localizer2;

     public TestController(IStringLocalizerFactory factory)
     {
         _localizer = factory.Create(typeof(SharedResource));
         _localizer2 = factory.Create("SharedResource", location: null);
     }       

     public IActionResult About()
     {
         ViewData["Message"] = _localizer["Your application description page."] 
             + " loc 2: " + _localizer2["Your application description page."];

         return View();
     }        

В примере выше показан каждый из двух методов создания фабрик.

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

// Dummy class to group shared resources

namespace Localization.StarterWeb
{
    public class SharedResource
    {
    }
}

Некоторые разработчики используют класс Startup, чтобы хранить глобальные или общие строки. В примере ниже используются локализаторы InfoController и SharedResource:

 public class InfoController : Controller
 {
     private readonly IStringLocalizer<InfoController> _localizer;
     private readonly IStringLocalizer<SharedResource> _sharedLocalizer;

     public InfoController(IStringLocalizer<InfoController> localizer,
                    IStringLocalizer<SharedResource> sharedLocalizer)
     {
         _localizer = localizer;
         _sharedLocalizer = sharedLocalizer;
     }

     public string TestLoc()
     {
         string msg = "Shared resx: " + _sharedLocalizer["Hello!"] +
                      " Info resx " + _localizer["Hello!"];
         return msg;
     }

Локализация представлений

Сервис IViewLocalizer предоставляет локализованные строки для представлений. Класс ViewLocalizer реализует этот интерфейс и находит местоположение ресурса из пути файла представления. В следующем коде показано, как использовать реализацию по умолчанию IViewLocalizer:

@using Microsoft.AspNet.Mvc.Localization

@inject IViewLocalizer Localizer

@{
    ViewData["Title"] = Localizer["About"];
}
<h2>@ViewData["Title"].</h2>
<h3>@ViewData["Message"]</h3>

<p>@Localizer["Use this area to provide additional information."]</p>

Реализация по умолчанию IViewLocalizer находит файл ресурса, основываясь на имени файла представления. Здесь нет опции для использования глобального общего файла ресурса. ViewLocalizer реализует локализатор, используя IHtmlLocalizer, так что Razor не создает HTML кодировку для локализованной строки. Вы можете параметризировать строку ресурса, и IViewLocalizer зашифрует в HTML параметры, но не строку ресурса. Рассмотрите следующую разметку Razor:

@Localizer["<i>Hello</i> <b>{0}!</b>", UserManager.GetUserName(User)]

Ресурсный файл для французского языка содержал бы следующее:

Ключ Значение
<i>Hello</i> <b>{0}!</b> <i>Bonjour</i> <b>{0}!</b>

Отображаемое представление содержало бы HTML разметку из ресурсного файла.

Note:Обычно мы локализуем текст, а не HTML.

Чтобы использовать в представлении общий ресурсный файл, внедрите IHtmlLocalizer<T>:

@using Microsoft.AspNet.Mvc.Localization
@using Localization.StarterWeb.Services

@inject IViewLocalizer Localizer
@inject IHtmlLocalizer<SharedResource> SharedLocalizer

@{
    ViewData["Title"] = Localizer["About"];
}
<h2>@ViewData["Title"].</h2>

<h1>@SharedLocalizer["Hello!"]</h1>

Локализация DataAnnotations

Сообщения об ошибках DataAnnotations локализуются с помощью IStringLocalizer<T>. Если используется опция ResourcesPath = "Resources", сообщения об ошибках в RegisterViewModel могут храниться вот здесь:

  • Resources/ViewModels.Account.RegisterViewModel.fr.resx
  • Resources/ViewModels/Account/RegisterViewModel.fr.resx
 public class RegisterViewModel
 {
     [Required(ErrorMessage = "The Email field is required.")]
     [EmailAddress(ErrorMessage = "The Email field is not a valid e-mail address.")]
     [Display(Name = "Email")]
     public string Email { get; set; }

     [Required(ErrorMessage = "The Password field is required.")]
     [StringLength(8, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
     [DataType(DataType.Password)]
     [Display(Name = "Password")]
     public string Password { get; set; }

     [DataType(DataType.Password)]
     [Display(Name = "Confirm password")]
     [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
     public string ConfirmPassword { get; set; }
 }

Рантайм не рассматривает локализованные строки на предмет не валидируемых атрибутов. В данном коде “Email” (из [Display(Name = "Email")]) не будет локализован.

Предоставление локализованных ресурсов для поддерживаемых вами языков и культур

SupportedCultures и SupportedUICultures

ASP.NET Core позволяет указать два значения для культуры, SupportedCultures и SupportedUICultures. Объект CultureInfo для SupportedCultures определяет результаты зависимых от культуры функций, работающих с датой, временем, числом и валютой. SupportedCultures также определяет порядок расположения текста, регистр при написании слов и сравнение строк. См. CultureInfo.CurrentCulture. SupportedUICultures определяет, какие строки из файлов .resx ищет ResourceManager. ResourceManager просто ищет строки для конкретной культуры, которые определены CurrentUICulture. У каждого потока в .NET есть объекты CurrentCulture и CurrentUICulture. ASP.NET Core проверяет эти значения при отображении зависимых от культуры функций. Например, если культура текущего потока установлена на “en-US” (English, United States), DateTime.Now.ToLongDateString() отображает “Thursday, February 18, 2016”, но если CurrentCulture установлена на “es-ES” (Spanish, Spain), результатом будет “jueves, 18 de febrero de 2016”.

Работа с ресурсными файлами

Ресурсный файл - это полезный механизм для отделения локализируемых строк от кода. Переводимые строки для языка не по умолчанию изолируются ресурсными файлами .resx. Например, вы хотите создать ресурсный файл для испанского языка с именем Welcome.es.resx, содержащим переводимые строки. “es” - это код языка для испанского. Чтобы создать этот ресурсный файл в Visual Studio:

  1. В Solution Explorer кликните правой клавишей мышки по папке, которая будет содержать ресурсный файл > Add > New Item.
../_images/newi.png
  1. В поле Search installed templates введите “resource” и имя файла.
../_images/res.png
  1. Введите ключевое значение (родную строку) в колонку Name, а переведенную строку в колонку Value.
../_images/hola.png

Visual Studio покажет файл Welcome.es.resx.

../_images/se.png

Генерирование ресурсных файлов с помощью Visual Studio

Если вы создаете ресурсный файл в Visual Studio без культуры в имени файла (например, Welcome.resx), Visual Studio создаст C# класс со свойством для каждой строки. Но это не совсем то, чего мы хотим от ASP.NET Core; как правило, у нас нет ресурсного файла по умолчанию .resx (Файла .resx без имени культуры). Мы предполагаем, что вы создаете .resx файл с именем культуры (например, Welcome.fr.resx). Когда вы создаете файл .resx с именем культуры, Visual Studio не будет создавать класс. мы надеемся, что вы не будете создавать ресурсный файл для языка по умолчанию.

Добавление других культур

Каждая комбинация языков и культур требует уникального ресурсного файла. Вы можете генерировать ресурсные файлы для различных культур и языков, создавая новые ресурсные файлы, в которых языковые коды ISO являются частью имени файла (например, en-us, fr-ca и en-gb). Эти ISO коды размещаются между именем файла и расширением .resx, как в Welcome.es-MX.resx (Spanish/Mexico). Если вы хотите указать язык, но не культуру, просто уберите код страны, например, Welcome.fr.resx для французского языка.

Реализация стратегии выбора языка/культуры для каждого запроса

Настройка локализации

Локализация настраивается в методе ConfigureServices:

  public void ConfigureServices(IServiceCollection services)
  {

      services.AddLocalization(options => options.ResourcesPath = "Resources");

      services.AddMvc()
        .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
        .AddDataAnnotationsLocalization();
  • AddLocalization добавляет сервисы локализации в контейнер. В коде выше путь ресурса установлен на “Resources”.
  • AddViewLocalization добавляет поддержку для локализованных файлов представления. В этом примере локализация основывается на суффиксе файла представления. Например, “fr” в файле Index.fr.cshtml.
  • AddDataAnnotationsLocalization добавляет поддержку для сообщений валидации DataAnnotations через абстракции IStringLocalizer.

Связующее ПО для локализации

Текущая культура для запроса настраивается в Связующее ПО (Middleware). Связующее ПО для локализации включается в методе Configure класса Startup.cs.

  public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)

      var supportedCultures = new[]
      {
          new CultureInfo("en-US"),
          new CultureInfo("en-AU"),
          new CultureInfo("en-GB"),
          new CultureInfo("en"),
          new CultureInfo("es-ES"),
          new CultureInfo("es-MX"),
          new CultureInfo("es"),
          new CultureInfo("fr-FR"),
          new CultureInfo("fr"),
      };

      app.UseRequestLocalization(new RequestLocalizationOptions
      {
          DefaultRequestCulture = new RequestCulture("en-US"),
          // Formatting numbers, dates, etc.
          SupportedCultures = supportedCultures,
          // UI strings that we have localized.
          SupportedUICultures = supportedCultures
      });

      // Remaining code omitted for brevity.

:dn:method:`~Microsoft.AspNetCore.Builder.ApplicationBuilderExtensions.UseRequestLocalization` инициализирует объект :dn:class:`~Microsoft.AspNetCore.Builder.RequestLocalizationOptions`. Для каждого запроса список RequestCultureProvider в RequestLocalizationOptions пронумерован, и тогда будет использоваться первый провайдер, который успешно определил культуру запроса. Провайдеры по умолчанию мы получаем из класса RequestLocalizationOptions:

  1. QueryStringRequestCultureProvider
  2. CookieRequestCultureProvider
  3. AcceptLanguageHeaderRequestCultureProvider

В списке по умолчанию провайдеры идут от более конкретных к менее конкретным. Далее мы рассмотрим, как можно изменить порядок следования и даже добавить пользовательский провайдер. Если ни один из провайдеров не может определить культуру запроса, используется DefaultRequestCulture.

QueryStringRequestCultureProvider

В некоторых приложениях используется строка запроса для настройки культуры и UI культуры. Для приложений, где используются куки или заголовок Accept-Language, добавление строки запроса в URL полезно для отладки и тестирования кода. По умолчанию QueryStringRequestCultureProvider регистрируется как первый провайдер локализации в списке RequestCultureProvider. Вы передаете параметры строки запроса culture и ui-culture. В следующем примере культура и язык устанавливаются на Spanish/Mexico:

http://localhost:5000/?culture=es-MX&ui-culture=es-MX

Если вы передаете только один параметр (culture или ui-culture), провайдер строки запроса установит оба значения, используя только одно, переданное вами. Например, если вы передадите только culture, установлен будет и Culture, и UICulture:

http://localhost:5000/?culture=es-MX

CookieRequestCultureProvider

В производственном приложении всегда существует механизм для настройки культуры с помощью куки культуры ASP.NET Core. Используйте метод :dn:method:`~Microsoft.AspNetCore.Localization.CookieRequestCultureProvider.MakeCookieValue`, чтобы создать куки.

:dn:cls:`~Microsoft.AspNetCore.Localization.CookieRequestCultureProvider` :dn:field:`~Microsoft.AspNetCore.Localization.CookieRequestCultureProvider.DefaultCookieName` возвращает имя куки по умолчанию, который используется для отслеживания информации о культуре, предпочитаемой пользователем. Именем куки по умолчанию является ”.AspNetCore.Culture”.

Вот формат куки: c=%LANGCODE%|uic=%LANGCODE%, где c - это Culture, а uic - это UICulture, например:

c=’en-UK’|uic=’en-US’

Если вы указываете только culture или UI culture, указанная культура будет использоваться и для culture, и для UI culture.

HTTP заголовок Accept-Language

Заголовок Accept-Language можно настроить в большинстве браузеров, и изначально он был направлен на определение языка пользователя. Заголовок определяет, что отправляет браузер или что он унаследовал от лежащей в основе операционной системы. Использование HTTP заголовка Accept-Language из браузерного запроса - это не совсем надежный путь определения языка, предпочитаемого пользователем (см. Настройка языка в браузере). В производственное приложение должен быть включен способ выбора пользователем нужной культуры.

Настройка HTTP заголовка Accept-Language в IE

  1. В настройках (значок шестеренки) нажмите Internet Options.
  2. Нажмите Languages.
../_images/lang.png
  1. Нажмите Set Language Preferences.
  2. Нажмите Add a language.
  3. Добавьте язык.
  4. Нажмите на языка, затем нажмите Move Up.

Использование пользовательского провайдера

Допустим, вы хотите, чтобы пользователи хранили предпочитаемые ими языки и культуры в вашей БД. Вы можете написать провайдер, который будет искать для них эти значения. Далее показано, как добавить пользовательский провайдер:

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

    options.DefaultRequestCulture = new RequestCulture(culture: "en-US", uiCulture: "en-US");
    options.SupportedCultures = supportedCultures;
    options.SupportedUICultures = supportedCultures;

    options.RequestCultureProviders.Insert(0, new CustomRequestCultureProvider(async context =>
    {
       // My custom request culture logic
      return new ProviderCultureResult("en");
    }));
});

Используйте RequestLocalizationOptions, чтобы добавлять или удалять провайдеры локализации.

Именование ресурсного файла

Ресурсы именуются по названию своего класса минус пространство имен по умолчанию (а также это имя сборки). Например, французский ресурс в проекте LocalizationWebsite.Web для класса LocalizationWebsite.Web.Startup назывался бы Startup.fr.resx. Класс LocalizationWebsite.Web.Controllers.HomeController был бы Controllers.HomeController.fr.resx. Если по какой-то причине нужный вам класс находится в том же проекте, но не в основном пространстве имен, вам потребуется прописать имя полностью. Например, в нашем проекте тип ExtraNamespace.Tools был бы ExtraNamespace.Tools.fr.resx.

В нашем примере метод ConfigureServices устанавливает ResourcesPath на “Resources”, так что относительной ссылкой на контроллер для французского ресурса является Resources/Controllers.HomeController.fr.resx. Кроме того, вы можете использовать папки для организации ресурсных файлов. Для HomeController путем был бы Resources/Controllers/HomeController.fr.resx. Если вы не используете опцию ResourcesPath, файл .resx пойдет в основную директорию проекта. Ресурсный файл для HomeController будет называться Controllers.HomeController.fr.resx. Выбор способа зависит от того, как вы хотите организовать ресурсные файлы.

Имя ресурса Точка или слэш
Resources/Controllers.HomeController.fr.resx Точка
Resources/Controllers/HomeController.fr.resx Слэш

Ресурсные файлы, использующие @inject IViewLocalizer в представлениях Razor, следуют схожему паттерну. Ресурсный файл для представления можно назвать, используя либо точку, либо слэш. Файлы представлений разор Razor имитируют путь связанных с ними файлов представлений. Допустим, мы установили ResourcesPath на “Resources”, французский ресурсный файл, связанный с представлением Views/Book/About.cshtml, мог бы именоваться вот так:

  • Resources/Views/Home/About.fr.resx
  • Resources/Views.Home.About.fr.resx

Если вы не используете опцию ResourcesPath, файл .resx для представления находился бы в той же папке, что и представление.

Если вы удалите указатель культуры ”.fr” И культура будет установлена на French (через куки или другой механизм), будет прочтен файл по умолчанию, а строки локализованы. Менеджер ресурсов определяет ресурс по умолчанию или резервный ресурс, если ничто не соответствует запрашиваемой культуре и вы создали *.resx файл без указателя культуры. Если при отсутствии ресурса для запрашиваемой культуры вы просто хотите вернуть ключ, у вас не должно быть ресурсного файла по умолчанию.

Программная настройка культуры

В этом проекте Localization.StarterWeb на GitHub содержится UI для настройки Culture. Файл Views/Shared/_SelectLanguagePartial.cshtml позволяет вам выбрать культуру из списка поддерживаемых культур:

@using Microsoft.AspNet.Builder
@using Microsoft.AspNet.Http.Features
@using Microsoft.AspNet.Localization
@using Microsoft.AspNet.Mvc.Localization
@using Microsoft.Extensions.Options

@inject IViewLocalizer Localizer
@inject IOptions<RequestLocalizationOptions> LocOptions

@{
    var requestCulture = Context.Features.Get<IRequestCultureFeature>();
    var cultureItems = LocOptions.Value.SupportedUICultures
        .Select(c => new SelectListItem { Value = c.Name, Text = c.DisplayName })
        .ToList();
}

<div title="@Localizer["Request culture provider:"] @requestCulture?.Provider?.GetType().Name">
    <form id="selectLanguage" asp-controller="Home" 
          asp-action="SetLanguage" asp-route-returnUrl="@Context.Request.Path" 
          method="post" class="form-horizontal" role="form">
        @Localizer["Language:"] <select name="culture"
          asp-for="@requestCulture.RequestCulture.UICulture.Name" asp-items="cultureItems">
        </select>
    </form>
</div>

Файл Views/Shared/_SelectLanguagePartial.cshtml добавлен в раздел footer в файл с версткой, так что он будет доступен всем представлениям:

  <div class="container body-content">
      @RenderBody()
      <hr />
      <footer>
          <div class="row">
              <div class="col-md-6">
                  <p>&copy; 2015 - Localization.StarterWeb</p>
              </div>
              <div class="col-md-6 text-right">
                  @await Html.PartialAsync("_SelectLanguagePartial")
              </div>
          </div>
      </footer>
  </div>

Метод SetLanguage устанавливает куки.

  [HttpPost]
  public IActionResult SetLanguage(string culture, string returnUrl)
  {
      Response.Cookies.Append(
          CookieRequestCultureProvider.DefaultCookieName,
          CookieRequestCultureProvider.MakeCookieValue(new RequestCulture(culture)),
          new CookieOptions { Expires = DateTimeOffset.UtcNow.AddYears(1) }
      );

      return LocalRedirect(returnUrl);
  }

Вы не можете просто подключить _SelectLanguagePartial.cshtml к коду этого проекта. У проекта Localization.StarterWeb на GitHub есть код для передачи RequestLocalizationOptions в Razor представление через контейнер Внедрение зависимостей (Dependency Injection).

Термины

Процесс локализации приложения требует базового понимания релевантных наборов символов, использующихся при разработке современных веб приложений. Хотя на всех компьютерах текст хранится в виде чисел (кодов), различные системы сохраняют один и тот же текст, используя разные числа. Процесс локализации касается перевода UI для конкретной культуры.

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

Формат RFC 4646 для имени культуры - это “<languagecode2>-<country/regioncode2>”, где <languagecode2> является кодом языка, а <country/regioncode2> - кодом субкультуры. Например, es-CL для Spanish (Chile), en-US для English (United States), а en-AU для English (Australia). RFC 4646 - это комбинация двубуквенного кода культуры, прописанного в нижнем регистре, ISO 639 (он связан с языком) и двубуквенного кода субкультуры, прописанного в верхнем регистре ISO 3166 (он связан со страной или регионом).

Вместо слова “интернационализация” (Internationalization) часто используют аббревиатуру “I18N”. В аббревиатуре находятся первая и последняя буквы слова, а также число букв между ними, то есть, 18 - это число букв между первой “I” и последней “N”. То же самое касается глобализации (Globalization - G11N) и локализации (Localization - L10N).

Термины:

  • Глобализация (G11N): процесс подготовки приложения к поддержке различных языков и культур.
  • Локализация (L10N): процесс настройки приложения для работы с конкретным языком и культурой.
  • Интернационализация (I18N): включает в себя глобализацию и локализацию.
  • Культура: обозначает язык и (дополнительно) регион.
  • Нейтральная культура: культура, для которой указан язык, но не регион, например, “en”, “es”.
  • Конкретная культура: культура, для которой указан язык и регион, например, “en-US”, “en-GB”, “es-CL”.

Дополнительные ресурсы

Поделись хорошей новостью с друзьями!
Следи за новостями!