10:39 0 0
Como hacer que tu API pueda soportar multiples idiomas How to make your API support multiple languages

Como hacer que tu API pueda soportar multiples idiomas How to make your API support multiple languages

  DrUalcman |  diciembre 262024

Hacer que las aplicaciones funcionen en varios idiomas es esencial para brindar una mejor experiencia de usuario en entornos multilingües. En .NET, IStringLocalizer es una herramienta muy útil para gestionar la localización. Sin embargo, a veces necesitamos un enfoque más personalizado que permita manejar los idiomas por solicitud sin alterar el estado global, como CultureInfo, lo cual puede generar problemas en escenarios multi-hilo.

Este artículo explica cómo he reemplazado IStringLocalizer con una abstracción personalizada, IStringCulture, para lograr localización específica de solicitud sin afectar la estructura general de nuestro código. Mi implementacion es muy similar a IStringLocalizer ya que yo tenía una API basada en esta abstracción y no queriía que el impacto fuera muy grande. Es por eso que apenas fue cambiar la inferzas utilizada en los casos de uso y no tuve que cambiar nada más en el código ya creado.

Nuestro Caso de Uso

Nuestra aplicación está construida con el patrón de arquitectura limpia utilizando .NET Minimal APIs. Nuestro objetivo era:

  1. Extraer el encabezado Accept-Language de las solicitudes HTTP.
  2. Proveer cadenas localizadas según este encabezado.
  3. Evitar alterar el CultureInfo global para garantizar que las solicitudes sean independientes entre sí.
  4. Minimizar el impacto en la base de código existente.

Para lograr esto, necesitábamos una capa de abstracción que se integrara perfectamente con el existente IStringLocalizer, pero que aislara la determinación del idioma en cada solicitud.

Visión General de la Solución

Presentamos una nueva interfaz, IStringCulture< TModel >, como un reemplazo directo de IStringLocalizer< TModel >. Esta interfaz encapsula la lógica de resolución del idioma y proporciona cadenas localizadas sin cambiar globalmente el CultureInfo.
¿Por qué Reemplazar IStringLocalizer?
  • IStringLocalizer utiliza CultureInfo para determinar el idioma actual. Este enfoque global puede causar conflictos en entornos multi-hilo donde solicitudes simultáneas requieren diferentes idiomas.
  • Necesitábamos aislar el idioma por solicitud para garantizar que una solicitud no afectara a otra.
Al crear IStringCulture, logramos:

  • Centralizar la lógica de determinación del idioma.
  • Manejo del idioma específico de la solicitud a través de un ThreadCultureScope temporal.
  • Una manera fluida de adaptar el uso existente de IStringLocalizer a nuestra nueva abstracción.

Manos a la obra, como lo implementé

1. Definir la Nueva Interfaz

Comenzamos creando la interfaz IStringCulture< TModel >, haciendo que se implementen los mismos métodos que en IStringLocalizer y ofreciendo uno extra por si en el código se necesite conocer la cultura en uso:

public interface IStringCulture< TModel >
{
    // Indexador para obtener cadenas localizadas por clave
    string this[string key] { get; }
    // Método explícito para obtener cadenas localizadas
    string GetString(string key);
    // Recupera el idioma de la solicitud actual
    string GetCurrentLanguage();
}

2. Implementación de la Clase

Nuestra implementación maneja la extracción del idioma y utiliza ThreadCultureScope para aislar los cambios de cultura temporalmente. Además, como en este case estamos en una API, hace uso de IHttpContextAccessor para obtener la solicitud actual y determinar el idioma en que la llamada se está realizando.

using Microsoft.Extensions.Localization;
using Microsoft.AspNetCore.Http;
internal class StringCulture< TModel >(IHttpContextAccessor contextAccessor, IStringLocalizer< TModel > localizer) : IStringCulture< TModel >
{
    public string this[string key]
    {
        get
        {
            using ThreadCultureScope cultureScope = new ThreadCultureScope(GetCurrentLanguage());
            return localizer[key];
        }
    }
    public string GetString(string key)
    {
        using ThreadCultureScope cultureScope = new ThreadCultureScope(GetCurrentLanguage());
        return localizer.GetString(key);
    }
    public string GetCurrentLanguage()
    {
        var httpContext = contextAccessor.HttpContext;
        string result = "en";
        if (httpContext is not null)
        {
            string acceptLanguage = httpContext.Request.Headers["Accept-Language"].ToString();
            string language = acceptLanguage.Split(',').FirstOrDefault() ?? "en";
            result = language;
        }
        return result;
    }
}

3. Manejo de Scopes Temporales

La clase ThreadCultureScope asegura cambios temporales de cultura localizados en el hilo actual:

public class ThreadCultureScope : IDisposable
{
    private readonly CultureInfo _originalCulture;
    private readonly CultureInfo _originalUICulture;
    public ThreadCultureScope(string language)
    {
        _originalCulture = Thread.CurrentThread.CurrentCulture;
        _originalUICulture = Thread.CurrentThread.CurrentUICulture;
        CultureInfo culture = new CultureInfo(language);
        Thread.CurrentThread.CurrentCulture = culture;
        Thread.CurrentThread.CurrentUICulture = culture;
    }
    public void Dispose()
    {
        Thread.CurrentThread.CurrentCulture = _originalCulture;
        Thread.CurrentThread.CurrentUICulture = _originalUICulture;
    }
}

4. Registrar Servicios en Program.cs

Para usar IStringCulture en la aplicación, registramos los servicios necesarios:

var builder = WebApplication.CreateBuilder(args);
// Agregar servicios
builder.Services.AddHttpContextAccessor();
builder.Services.AddSingleton(typeof(IStringCulture< >), typeof(StringCulture< >));
// Configurar recursos de localización
builder.Services.AddLocalization();
var app = builder.Build();
app.Run();

5. Uso de la Nueva Abstracción

Aqui un ejemplo de cómo un controlador o endpoints de Minimal API para usar IStringCulture en lugar de IStringLocalizer y mostrar un texto en el idioma que nos entre:

app.MapPost("/process-request", async (IStringCulture localizer) =>
{
    string message = localizer["WelcomeMessage"];
    return Results.Ok(new { message });
});

Recuerda que si estas en un proyecto limpio, respetando la inversión de dependencias, podrías utilizar IStringCulture en tu caso de uso, y listo.

Beneficios de Este Enfoque

  1. Localización por Solicitud: Los cambios de idioma están aislados para cada solicitud, garantizando la seguridad en hilos.
  2. Integración Transparente: Diseñamos IStringCulture para imitar IStringLocalizer, logrando mínimos cambios de código Además de un fácil aprendizaje para otros.
  3. Lógica Centralizada: La lógica de determinación de idioma ahora está centralizada en StringCulture, mejorando el mantenimiento.
  4. Extensibilidad: Futuros cambios (por ejemplo, recuperar idiomas de cookies) pueden hacerse en un solo lugar sin tocar otras partes del código.

Conclusión

Refactorizar IStringLocalizer a IStringCulture nos permitió abordar limitaciones clave en un entorno multi-hilo mientras asegurábamos un impacto mínimo en la base de código existente. Este patrón es invaluable para aplicaciones que requieren localización específica por solicitud.

Al aislar la determinación del idioma y mantener la compatibilidad con IStringLocalizer, logramos una solución escalable y limpia que puede adaptarse a futuros requisitos.

English version

This english version is translated and review using AI, my apologies if some it's not very good.

In modern web applications, proper localization is essential for enhancing user experience in multi-language environments. .NET's IStringLocalizer is a powerful tool to handle this. However, in some cases, we may need a more customized approach to manage request-specific languages without interfering with global state, such as CultureInfo, which can be problematic in multi-threaded scenarios.

This article describes how we replaced IStringLocalizer with a custom abstraction, IStringCulture, for better request-scoped localization without impacting the overall structure of our code.

Our Use Case

Our application is built using a clean architecture pattern with .NET Minimal APIs. The goal is to:

  1. Extract the Accept-Language header from HTTP requests.
  2. Provide localized strings based on this header.
  3. Avoid altering global CultureInfo to ensure multi-threaded request independence.
  4. Maintain minimal impact on the existing codebase.

To achieve this, we needed a layer of abstraction that integrates seamlessly with the existing IStringLocalizer but isolates language determination to each request.

Solution Overview

We introduced a new interface, IStringCulture< TModel >, as a drop-in replacement for IStringLocalizer< TModel >. This interface encapsulates language resolution and provides localized strings without globally changing the CultureInfo.

Why Replace IStringLocalizer?

IStringLocalizer uses CultureInfo for determining the current language. This global approach can lead to conflicts in multi-threaded environments where simultaneous requests require different languages.

  • We needed per-request language isolation to ensure that one request does not affect another.
  • By creating IStringCulture, we achieve the following:

Centralized language resolution logic.

  • Per-request culture management through a temporary ThreadCultureScope.
  • A seamless way to adapt the existing usage of IStringLocalizer to our new abstraction.

How I did the Implementation

1. Defining the New Interface

We started by creating the IStringCulture interface:

Check code above

2. Implementing the Class

Our implementation handles language extraction and uses ThreadCultureScope to isolate culture changes.

Check code above

3. Handling Temporary Culture Scopes

The ThreadCultureScope class ensures temporary, thread-local culture changes for localization:

Check code above

4. Registering Services in Program.cs

To use IStringCulture in the application, we registered the necessary services:

Check code above

5. Using the New Abstraction

We modified controllers or Minimal API endpoints to use IStringCulture instead of IStringLocalizer:

Check code above

Remember that if you are in a clean project, respecting the dependency inversion, you could use IStringCulture in your use case, and that's it.

Benefits of This Approach

  1. Request-Scoped Localization: Language changes are isolated to each request, ensuring thread safety.
  2. Seamless Integration: By designing IStringCulture to mimic IStringLocalizer, we achieved minimal code changes in existing implementations.
  3. Centralized Logic: Language determination logic is now centralized in StringCulture, improving maintainability.
  4. Extensibility: Future changes (e.g., retrieving languages from cookies) can be made in one place without touching other parts of the codebase.

Conclusion

Refactoring IStringLocalizer to IStringCulture allowed us to address key limitations in a multi-threaded environment while ensuring minimal impact on the existing codebase. This pattern proves invaluable for applications requiring request-specific localization.

By isolating the language determination and maintaining compatibility with IStringLocalizer, we achieved a scalable and clean solution that can adapt to future requirements.

0 Comentarios

 
 
 

Archivo