Creando Web APIs multidiomas en ASP.NET Core

En este artículo, te enseñaré a desarrollar APIs en varios idiomas en ASP.NET Core, a través de fundamentos y ejemplos prácticos.

Te mostrare como hacer que tu API devuelva resultados en el idioma preferido del usuario.

Aprenderemos a preparar al aplicación para la globalización y localización, a crear y administrar archivos de recursos, a usar las traducciones en el código de la aplicación y por último, veremos las estrategias de selección de idiomas.

Preparación de la aplicación para la globalización

Configuración de servicios de localización

Creamos nuevo proyecto para nuestra API en ASP.NET Core.

Configuramos los servicios necesarios para la globalización. Cuando hablamos de globalización nos referimos al proceso de hacer que una aplicación admita diferentes idiomas y regiones, formatos de fecha, monedas, etc.

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

«AddLocalization(…)» configura los servicios que después podremos utilizar en nuestros componentes y controladores.

«ResourcesPath» especifica la carpeta que contiene los archivos de recursos.

Configuración del middleware de localización

«UseRequestLocalization()» es el middleware que nos permite gestionar las preferencias de idioma y cultura de los usuarios y así poder ajustar la aplicación.

app.UseRequestLocalization();

Su función principal es detectar y establecer la cultura para cada solicitud HTTP entrante en función de la preferencia del usuario, la información del navegador o cualquier otra lógica personalizada que se defina, generalmente basándose en las cabeceras HTTP como Accept-Language y otras opciones como QueryString o mediante cookies, que veremos más adelante.

Configuración de las opciones de localización

Usamos «builder.Services.Configure<RequestLocalizationOptions>(…)» para configurar opciones de localización.

builder.Services.Configure<RequestLocalizationOptions>(options =>
{
	var supportedCultures = new[] { "en-US", "es-ES" };
	options.SetDefaultCulture(supportedCultures[0])
		     .AddSupportedCultures(supportedCultures)
		     .AddSupportedUICultures(supportedCultures);
});

«RequestLocalizationOptions» es la clase que se utiliza para configurar las opciones de localización de la solicitud.

«supportedCultures[…]» configura la cultura predeterminada que se utilizará si no se especifica ninguna preferencia de cultura en la solicitud.

«AddSupportedCultures(…)» establece la lista de culturas admitidas por la aplicación.

«AddSupportedUICultures(…)» establece las culturas admitidas para la interfaz de usuario (UI).

Creación y administración de archivos de recursos

Cuando hablamos de localización nos referimos al proceso de personalización de una aplicación para regiones e idiomas específicos. Esto implica la traducción de textos y contenidos a los idiomas admitidos.

Creamos ahora los archivos de recursos, que son las cadenas de texto para cada referencia cultural, la referencia cultural es un código de idioma y opcionalmente de país o región («en», «en-US»).

Códigos de idioma y de país o región: <language code>-<country/region code>

Anteriormente, configuramos la carpeta “Resources” para contener los archivos de recursos.

Aclarar que podemos usar la nomenclatura de punto:

Backend
├── Resources
│   ├── Controllers.AboutController.en-US.resx   (Inglés estadounidense)
│   ├── Controllers.AboutController.en.resx   (Inglés)
│   ├── Controllers.AboutController.es-ES.resx   (Español de España)
│   └── Controllers.AboutController.es.resx   (Español)
└── ...

o nomenclatura ruta de acceso para organizar los archivos de recursos:

Backend
├── Resources
│   ├── Controllers
│       ├── AboutController.en-US.resx   (Inglés estadounidense)
│       ├── AboutController.en.resx   (Inglés)
│       ├── AboutController.es-ES.resx   (Español de España)
│       └── AboutController.es.resx   (Español)
└── ...

En nuestro caso, usaremos la nomenclatura de ruta de acceso para organizar los archivos de recursos para los distintos idiomas.

Una vez creados los recursos, los abrimos y escribimos el valor de clave en la columna «nombre» y la cadena traducida en la columna «valor»:

Ya podemos empezar a usar nuestros recursos en el controlador de nuestra Web API💪

Uso de traducciones en el código de la aplicación

Uso de IStringLocalizer para localizar cadenas de texto

«IStringLocalizer<T>» nos permite usar traducciones en el código de la aplicación en función de la cultura actual.

Para nuestro ejemplo, lo usaremos en el controlador, inyectando el servicio y usando las cadenas de texto.

[ApiController]
[Route("api/[controller]")]
public class AboutController : ControllerBase
{
    private readonly IStringLocalizer<AboutController> _localizer;

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

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

Uso de las traducciones en mensajes de error

También podemos usarlo para devolver los mensajes de error que la API devuelve al usuario o al desarrollador cuando ocurre un problema, como «Error 404: Recurso no encontrado» o «Error de autenticación», pueden requerir traducción si la API está destinada a usuarios o desarrolladores que hablan diferentes idiomas.

[ApiController]
[Route("api/[controller]")]
public class AboutController : ControllerBase
{
    ...
    [HttpGet("GetResource")]
    public IActionResult GetResource()
    {
        //error 404 not found
        return NotFound(_localizer["Resource not found"].Value);
    }
}

Uso de las traducciones con DataAnnotations

Podemos adaptar los mensajes de validación y etiquetas generados automáticamente por las anotaciones de validación de DataAnnotations a diferentes idiomas y regiones, permitiendo que los mensajes se muestren según las preferencias culturales de los usuarios.

public class User
{
    [Required(ErrorMessage = "The Name field is required.")]
    [StringLength(50, ErrorMessage = "The Name field cannot be more than 50 characters.")]
    public string Name { get; set; }

    [Required(ErrorMessage = "The Email field is required.")]
    [EmailAddress(ErrorMessage = "The Email field does not have a valid format.")]
    public string Email { get; set; }
}

Necesitamos añadir a program.cs método «.AddDataAnnotationsLocalization(…)» que se utiliza para configurar la localización de mensajes de validación basados en Data Annotations en la aplicación.

builder.Services.AddControllers()
    .AddDataAnnotationsLocalization(options => {
        options.DataAnnotationLocalizerProvider = (type, factory) =>
            factory.Create(typeof(SharedResource));
    });

«options.DataAnnotationLocalizerProvider» aquí, se establece el proveedor de localización de anotaciones de datos. Esto significa que, cuando se necesite localizar mensajes de validación, se utilizará un recurso compartido (clase SharedResource) como fuente de localización.

Creamos los archivos de recursos y las cadenas de traducciones:

Por último, creamos la acción en el controlador que maneja las solicitudes HTTP POST:

[Route("api/[controller]")]
[ApiController]
public class UserController : ControllerBase
{
    [HttpPost]
    public IActionResult Post([FromBody] User user)
    {
        if (!ModelState.IsValid)
        {
            return BadRequest(ModelState);
        }

        return Ok();
    }
}

Aquí se verifica si el modelo "User" pasado como parámetro cumple con las reglas de validación definidas en su clase y en el caso de no cumplirlas muestra los mensaje de validación en el idioma del usuario.

Recursos compartidos entre componentes

Los recursos compartidos «Shared Resources» se refieren a archivos de recursos que contienen cadenas de texto que son utilizadas en múltiples componentes de una aplicación. La idea detrás de los recursos compartidos es centralizar y reutilizar las traducciones en diferentes componentes de la aplicación.

Creamos los archivos de recursos compartidos en la carpeta «Resources»:

Backend
├── Resources
│   ├── SharedResource.en-US.resx   (Inglés estadounidense)
│   ├── SharedResource.en.resx   (Inglés)
│   ├── SharedResource.es-ES.resx   (Español de España)
│   ├── SharedResource.es.resx   (Español)
│   └── ...
└── ...

Definimos la Clase «SharedResource» que se usa como marcador para los recursos compartidos.

namespace Backend
{
	public class SharedResource
	{
	}
}

Y ya podemos usarlo en los distintos controladores de la aplicación:

[ApiController]
[Route("api/[controller]")]
public class HomeController : ControllerBase
{
    private readonly IStringLocalizer<SharedResource> _sharedLocalizer;

    public HomeController(IStringLocalizer<SharedResource> sharedLocalizer)
    {
        _sharedLocalizer = sharedLocalizer;
    }

    [HttpGet]
    public IActionResult Get()
    {
        return Ok(_sharedLocalizer["Your application shared resources."].Value);
    }
}

Estrategias de selección de idioma o referencia cultural

El idioma hace referencia a las selecciones realizadas por un usuario en la configuración del navegador, cookies, parámetros de consulta y otras fuentes, pero la aplicación establece en última instancia la propiedad CurrentCulture («referencia cultural») del idioma solicitado por el usuario.

«RequestCultureProviders» nos permiten que la aplicación determine la cultura preferida del usuario en función de información proporcionada por el navegador, cookies, parámetros de consulta y otras fuentes. Podemos configurar múltiples proveedores, hay que tener en cuenta que el orden en el que se agregan estos proveedores afecta a la prioridad con la que se elige la cultura preferida del usuario. Los RequestCultureProviders se evalúan en el orden en que están registrados en la configuración de localización de la aplicación.

Detección de idioma a través del encabezado HTTP Accept-Language

AcceptLanguageHeaderRequestCultureProvider este proveedor determinará la cultura preferida basada en la cabecera Accept-Language en la solicitud del navegador.

builder.Services.Configure<RequestLocalizationOptions>(options => {
    var SupportedCultures = new[] { "en-US", "es-ES" };
    options.SetDefaultCulture(SupportedCultures[0])
            .AddSupportedCultures(SupportedCultures)
            .AddSupportedUICultures(SupportedCultures)
            .RequestCultureProviders = new List<IRequestCultureProvider>
            {
                new AcceptLanguageHeaderRequestCultureProvider(),
                ...
            };
});

Para jugar con las distintos idiomas acceder a la configuración del navegador y cambiar el idioma.

Selección de idioma a través de QueryString

QueryStringRequestCultureProvider este proveedor determinará la cultura preferida basada en los valores de la cadena de consulta (query string) de la URL.

.RequestCultureProviders = new List<IRequestCultureProvider>
{
   new QueryStringRequestCultureProvider(),
   ...
};

Para cambiar de idioma, modificar la cadena de consulta, por ejemplo, localhost/api/home?culture=en-US, localhost/api/home?culture=es-ES

CookieRequestCultureProvider este proveedor determinará la cultura preferida basada en una cookie en la solicitud.

.RequestCultureProviders = new List<IRequestCultureProvider>
{
   new CookieRequestCultureProvider(),
   ...
};

Para cambiar de idioma, llamar al método «SetLanguage»

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

    return Ok();
}

Para eliminar la Cookie, llamar a «ClearLanguage»

[HttpDelete("ClearLanguage")]
public IActionResult ClearLanguage()
{
    Response.Cookies.Delete(CookieRequestCultureProvider.DefaultCookieName);
    return Ok();
}

Selección de idioma a través de la ruta

RouteDataRequestCultureProvider este proveedor determinará la cultura preferida basada en los valores de ruta de la URL.

.RequestCultureProviders = new List<IRequestCultureProvider>
{
   new RouteDataRequestCultureProvider(),
   ...
};
[Route("api/{culture}/[controller]")]
[ApiController]
public class RouteController : ControllerBase
{
    private readonly IStringLocalizer<RouteController> _localizer;

    public RouteController(IStringLocalizer<RouteController> sharedLocalizer)
    {
        _localizer = sharedLocalizer;
    }

    [HttpGet]
    public IActionResult Get()
    {
        return Ok(_localizer["The route language is {0}."].Value);
    }
}

Para cambiar de idioma, modificar la ruta, por ejemplo, localhost/api/es/route, localhost/api/es-ES/route, localhost/api/en-US/route

Selección de idioma personalizado

CustomRequestCultureProvider este proveedor personalizado podría implementar lógicas específicas para determinar la cultura preferida.

.RequestCultureProviders = new List<IRequestCultureProvider>
{
    new CustomRequestCultureProvider(async context =>
    {
        // Aquí lógica para determinar la cultura

        // En este ejemplo, se simula que se obtiene la cultura "en-US" como predeterminada.
        string culture = "es-ES";

        return new ProviderCultureResult(culture);
    }),
   ...
};

Para jugar con las distintas formas de selección de idioma, ir comentando proveedores y dejar solo con el que se quiera probar. También se puede probar más de un proveedor, solo hay que tener en cuenta el orden.

.RequestCultureProviders = new List<IRequestCultureProvider>
{
    new AcceptLanguageHeaderRequestCultureProvider(),
    new QueryStringRequestCultureProvider(),
    //new CookieRequestCultureProvider(),
    //new RouteDataRequestCultureProvider(),
    //new CustomRequestCultureProvider(async context => {...})
};

Todas las implementaciones de estos ejemplos están disponibles en el código de GitHub, que podéis descargar.

Eso es todo, por el momento, seguiré actualizando y agregando contenido en este post. Espero que te haya resultado interesante😉

Código de ejemplo

Localization in ASP.NET Core

2
1