Hacer que el contenido de una aplicación de ASP.NET Core sea localizable

Por Hisham Bin Ateya, Damien Bowden, Bart Calixto y Nadeem Afana

Una de las tareas para localizar una aplicación es encapsular el contenido localizable con código, lo que facilita reemplazar ese contenido por distintas referencias culturales.

IStringLocalizer

IStringLocalizer y IStringLocalizer<T> se diseñaron para mejorar la productividad al desarrollar aplicaciones localizadas. IStringLocalizer usa ResourceManager y ResourceReader para proporcionar recursos específicos de la referencia cultural en tiempo de ejecución. La interfaz tiene un indizador y un IEnumerable para devolver las cadenas localizadas. IStringLocalizer no necesita que se almacenen las cadenas de idioma predeterminado en un archivo de recursos. Puede desarrollar una aplicación destinada a la localización sin necesidad de crear archivos de recursos al principio de la fase de desarrollo.

En el ejemplo de código siguiente se muestra cómo encapsular la cadena "About Title" para la localización.

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;

namespace Localization.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"];
    }
}

En el código anterior, la implementación de IStringLocalizer<T> procede de la inserción de dependencias. Si no se encuentra el valor localizado de "About Title, se devuelve la clave de indizador, es decir, la cadena "About Title".

Puede dejar las cadenas literales del idioma predeterminado en la aplicación y ajustarlas en el localizador, de modo que se pueda centrar en el desarrollo de la aplicación. Desarrolle una aplicación con el idioma predeterminado y prepárela para el proceso de localización sin necesidad de crear primero un archivo de recursos predeterminado.

También puede seguir el método tradicional y proporcionar una clave para recuperar la cadena de idioma predeterminado. El nuevo flujo de trabajo, que carece de archivo .resx de idioma predeterminado y simplemente ajusta los literales de cadena, puede ahorrar a muchos desarrolladores la sobrecarga de localizar una aplicación. Otros desarrolladores prefieren el flujo de trabajo tradicional, ya que puede facilitar el trabajo con literales de cadena largos, así como la actualización de las cadenas localizadas.

IHtmlLocalizer

Use la implementación de IHtmlLocalizer<TResource> para los recursos que contienen HTML. El HTML IHtmlLocalizer codifica los argumentos a los que se da formato en la cadena de recursos, pero no codifica como HTML la cadena de recursos en sí misma. En el código resaltado siguiente, solo el valor del parámetro name está codificado en HTML.

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

namespace Localization.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();
    }

NOTA: Por lo general, solo se localiza texto, no HTML.

IStringLocalizerFactory

En el nivel mínimo, IStringLocalizerFactory se puede recuperar de la inserción de dependencias:

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

    public TestController(IStringLocalizerFactory factory)
    {
        var type = typeof(SharedResource);
        var assemblyName = new AssemblyName(type.GetTypeInfo().Assembly.FullName);
        _localizer = factory.Create(type);
        _localizer2 = factory.Create("SharedResource", assemblyName.Name);
    }       

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

        return View();
    }

En el código anterior se muestran los dos métodos factory.Create.

Recursos compartidos

Puede dividir las cadenas localizadas por controlador o área, o bien tener un solo contenedor. En la aplicación de ejemplo, se usa una clase de marcador denominada SharedResource para los recursos compartidos. Nunca se llama a la clase de marcador:

// Dummy class to group shared resources

namespace Localization;

public class SharedResource
{
}

En el ejemplo siguiente, se usan los localizadores InfoController y 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;
    }

Localización de vista

El servicio IViewLocalizer proporciona cadenas localizadas para una vista. La clase ViewLocalizer implementa esta interfaz y busca la ubicación del recurso en la ruta de acceso del archivo de vista. En el código siguiente se muestra cómo usar la implementación predeterminada de IViewLocalizer:

@using Microsoft.AspNetCore.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>

La implementación predeterminada de IViewLocalizer busca el archivo de recursos según el nombre del archivo de vista. No se puede usar un archivo de recursos compartidos global. ViewLocalizer implementa el localizador mediante IHtmlLocalizer, por lo que Razor no codifica como HTML la cadena localizada. Puede parametrizar las cadenas de recursos y IViewLocalizer codifica como HTML los parámetros, pero no la cadena de recursos. Observe el siguiente marcado Razor:

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

Un archivo de recursos en francés puede contener los valores siguientes:

Clave Valor
<i>Hello</i> <b>{0}!</b> <i>Bonjour</i> <b>{0} !</b>

La vista representada contendría el marcado HTML del archivo de recursos.

Por lo general, solo se localiza texto, no HTML.

Para usar un archivo de recursos compartido en una vista, inserte IHtmlLocalizer<T>:

@using Microsoft.AspNetCore.Mvc.Localization
@using Localization.Services

@inject IViewLocalizer Localizer
@inject IHtmlLocalizer<SharedResource> SharedLocalizer

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

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

Localización de DataAnnotations

Los mensajes de error de DataAnnotations se localizan con IStringLocalizer<T>. Mediante la opción ResourcesPath = "Resources", es posible almacenar los mensajes de error en RegisterViewModel en cualquiera de las rutas de acceso siguientes:

  • Resources/ViewModels.Account.RegisterViewModel.fr.resx
  • Resources/ViewModels/Account/RegisterViewModel.fr.resx
using System.ComponentModel.DataAnnotations;

namespace Localization.ViewModels.Account;

public class RegisterViewModel
{
    [Required(ErrorMessage = "The Email field is required.")]
    [EmailAddress(ErrorMessage = "The Email field is not a valid email 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; }
}

Los atributos que no son de validación están localizados.

Cómo usar una cadena de recursos para varias clases

En el código siguiente se muestra cómo usar una cadena de recursos para atributos de validación con varias clases:

    services.AddMvc()
        .AddDataAnnotationsLocalization(options => {
            options.DataAnnotationLocalizerProvider = (type, factory) =>
                factory.Create(typeof(SharedResource));
        });

En el código anterior, SharedResource es la clase correspondiente al archivo .resx donde se almacenan los mensajes de validación. Según este enfoque, DataAnnotations solo usa SharedResource, en lugar del recurso de cada clase.

Configuración de los servicios de localización

Los servicios de localización se configuran en Program.cs:

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

builder.Services.AddMvc()
    .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
    .AddDataAnnotationsLocalization();
  • AddLocalization agrega los servicios de localización al contenedor de servicios, incluidas las implementaciones de IStringLocalizer<T> y IStringLocalizerFactory. El código anterior también establece la ruta de acceso a los recursos en "Resources".

  • AddViewLocalization agrega compatibilidad con los archivos de vista localizados. En este ejemplo, la localización de vista se basa en el sufijo del archivo de vista. Por ejemplo, "fr" en el archivo Index.fr.cshtml.

  • AddDataAnnotationsLocalization agrega compatibilidad con mensajes de validación de DataAnnotations localizados mediante abstracciones IStringLocalizer.

Nota

Es posible que no pueda escribir comas decimales en campos decimales. Para que la validación de jQuery sea compatible con configuraciones regionales distintas del inglés que usan una coma (",") en lugar de un punto decimal y formatos de fecha distintos del de Estados Unidos, debe seguir unos pasos para globalizar la aplicación. Consulte este comentario 4076 de GitHub para instrucciones sobre cómo agregar la coma decimal.

Pasos siguientes

La localización de una aplicación también implica las tareas siguientes:

Recursos adicionales

By Rick Anderson, Damien Bowden, Bart Calixto, Nadeem Afana y Hisham Bin Ateya

Una de las tareas para localizar una aplicación es encapsular el contenido localizable con código, lo que facilita reemplazar ese contenido por distintas referencias culturales.

IStringLocalizer

IStringLocalizer y IStringLocalizer<T> se diseñaron para mejorar la productividad al desarrollar aplicaciones localizadas. IStringLocalizer usa ResourceManager y ResourceReader para proporcionar recursos específicos de la referencia cultural en tiempo de ejecución. La interfaz tiene un indizador y un IEnumerable para devolver las cadenas localizadas. IStringLocalizer no necesita que se almacenen las cadenas de idioma predeterminado en un archivo de recursos. Puede desarrollar una aplicación destinada a la localización sin necesidad de crear archivos de recursos al principio de la fase de desarrollo.

En el ejemplo de código siguiente se muestra cómo encapsular la cadena "About Title" para la localización.

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;

namespace Localization.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"];
        }
    }
}

En el código anterior, la implementación de IStringLocalizer<T> procede de la inserción de dependencias. Si no se encuentra el valor localizado de "About Title, se devuelve la clave de indizador, es decir, la cadena "About Title".

Puede dejar las cadenas literales del idioma predeterminado en la aplicación y ajustarlas en el localizador, de modo que se pueda centrar en el desarrollo de la aplicación. Desarrolle una aplicación con el idioma predeterminado y prepárela para el proceso de localización sin necesidad de crear primero un archivo de recursos predeterminado.

También puede seguir el método tradicional y proporcionar una clave para recuperar la cadena de idioma predeterminado. El nuevo flujo de trabajo, que carece de archivo .resx de idioma predeterminado y simplemente ajusta los literales de cadena, puede ahorrar a muchos desarrolladores la sobrecarga de localizar una aplicación. Otros desarrolladores prefieren el flujo de trabajo tradicional, ya que puede facilitar el trabajo con literales de cadena largos, así como la actualización de las cadenas localizadas.

IHtmlLocalizer

Use la implementación de IHtmlLocalizer<T> para los recursos que contienen HTML. El HTML IHtmlLocalizer codifica los argumentos a los que se da formato en la cadena de recursos, pero no codifica como HTML la cadena de recursos en sí misma. En el código resaltado siguiente, solo el valor del parámetro name está codificado en HTML.

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

namespace Localization.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();
        }

Nota

Por lo general, solo se localiza texto, no HTML.

IStringLocalizerFactory

En el nivel más bajo, puede obtener IStringLocalizerFactory de la inserción de dependencias:

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

        public TestController(IStringLocalizerFactory factory)
        {
            var type = typeof(SharedResource);
            var assemblyName = new AssemblyName(type.GetTypeInfo().Assembly.FullName);
            _localizer = factory.Create(type);
            _localizer2 = factory.Create("SharedResource", assemblyName.Name);
        }       

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

En el código anterior se muestran los dos métodos factory.Create.

Recursos compartidos

Puede dividir las cadenas localizadas por controlador o área, o bien tener un solo contenedor. En la aplicación de ejemplo, se usa una clase ficticia denominada SharedResource para los recursos compartidos.

// Dummy class to group shared resources

namespace Localization
{
    public class SharedResource
    {
    }
}

Algunos programadores usan la clase Startup para contener cadenas globales o compartidas. En el ejemplo siguiente, se usan los localizadores InfoController y 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;
    }

Localización de vista

El servicio IViewLocalizer proporciona cadenas localizadas para una vista. La clase ViewLocalizer implementa esta interfaz y busca la ubicación del recurso en la ruta de acceso del archivo de vista. En el código siguiente se muestra cómo usar la implementación predeterminada de IViewLocalizer:

@using Microsoft.AspNetCore.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>

La implementación predeterminada de IViewLocalizer busca el archivo de recursos según el nombre del archivo de vista. No se puede usar un archivo de recursos compartidos global. ViewLocalizer implementa el localizador mediante IHtmlLocalizer, por lo que Razor no codifica como HTML la cadena localizada. Puede parametrizar las cadenas de recursos y IViewLocalizer codifica como HTML los parámetros, pero no la cadena de recursos. Observe el siguiente marcado Razor:

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

Un archivo de recursos en francés puede contener los valores siguientes:

Clave Valor
<i>Hello</i> <b>{0}!</b> <i>Bonjour</i> <b>{0} !</b>

La vista representada contendría el marcado HTML del archivo de recursos.

Nota

Por lo general, solo se localiza texto, no HTML.

Para usar un archivo de recursos compartido en una vista, inserte IHtmlLocalizer<T>:

@using Microsoft.AspNetCore.Mvc.Localization
@using Localization.Services

@inject IViewLocalizer Localizer
@inject IHtmlLocalizer<SharedResource> SharedLocalizer

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

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

Localización de DataAnnotations

Los mensajes de error de DataAnnotations se localizan con IStringLocalizer<T>. Mediante la opción ResourcesPath = "Resources", es posible almacenar los mensajes de error en RegisterViewModel en cualquiera de las rutas de acceso siguientes:

  • 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 email 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; }
}

En ASP.NET Core MVC 1.1.0 y versiones posteriores, los atributos que no son de validación están localizados.

Cómo usar una cadena de recursos para varias clases

En el código siguiente se muestra cómo usar una cadena de recursos para atributos de validación con varias clases:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc()
        .AddDataAnnotationsLocalization(options => {
            options.DataAnnotationLocalizerProvider = (type, factory) =>
                factory.Create(typeof(SharedResource));
        });
}

En el código anterior, SharedResource es la clase correspondiente al archivo .resx donde se almacenan los mensajes de validación. Según este enfoque, DataAnnotations solo usa SharedResource, en lugar del recurso de cada clase.

Configuración de los servicios de localización

Los servicios de localización se configuran en el método Startup.ConfigureServices:

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

services.AddMvc()
    .AddViewLocalization(LanguageViewLocationExpanderFormat.Suffix)
    .AddDataAnnotationsLocalization();
  • AddLocalization agrega los servicios de localización al contenedor de servicios, incluidas las implementaciones de IStringLocalizer<T> y IStringLocalizerFactory. El código anterior también establece la ruta de acceso a los recursos en "Resources".

  • AddViewLocalization agrega compatibilidad con los archivos de vista localizados. En este ejemplo, la localización de vista se basa en el sufijo del archivo de vista. Por ejemplo, "fr" en el archivo Index.fr.cshtml.

  • AddDataAnnotationsLocalization agrega compatibilidad con mensajes de validación de DataAnnotations localizados mediante abstracciones IStringLocalizer.

Nota

Es posible que no pueda escribir comas decimales en campos decimales. Para que la validación de jQuery sea compatible con configuraciones regionales distintas del inglés que usan una coma (",") en lugar de un punto decimal y formatos de fecha distintos del de Estados Unidos, debe seguir unos pasos para globalizar la aplicación. Consulte este comentario 4076 de GitHub para instrucciones sobre cómo agregar la coma decimal.

Pasos siguientes

La localización de una aplicación también implica las tareas siguientes:

Recursos adicionales