Archivo de la etiqueta: .NET Core

Localizando desde la base de datos


En .Net Core, la interfaz IStringLocalizer, permite estandarizar la forma en que se presentan los textos según el idioma del usuario.

En general, todos los ejemplos se basan en la utilización de archivos de recursos.

En este caso, veremos cómo podemos, implementando la interfaz, obtener los textos desde una tabla de base de datos, de modo tal de poder ir incrementando dinámicamente los idiomas a los que damos soporte.

Se utiliza una tabla para contener las cadenas, con, al menos, el siguiente esquema.

CREATE TABLE [Masters].[AppResources](
	[Culture] [nvarchar](20) NOT NULL,
	[Key] [nvarchar](50) NOT NULL,
	[StringText] [nvarchar](max) NULL,
 CONSTRAINT [PK_AppResources] PRIMARY KEY CLUSTERED 
(
	[Culture] ASC,
	[Key] ASC
)

Definimos entonces una clase que implemente la interfaz Microsoft.Extensions.Localization.IStringLocalizer.

La interfaz expone directamente las propiedades de su contenido como indexador, con el método this[Clave]

/// <summary>
/// Represents a service that provides localized strings.
/// </summary>
public interface IStringLocalizer
{
    /// <summary>
    /// Gets the string resource with the given name.
    /// </summary>
    /// <param name="name">The name of the string resource.</param>
    /// <returns>The string resource as a <see cref="LocalizedString"/>.</returns>
    LocalizedString this[string name] { get; }

    /// <summary>
    /// Gets the string resource with the given name and formatted with the supplied arguments.
    /// </summary>
    /// <param name="name">The name of the string resource.</param>
    /// <param name="arguments">The values to format the string with.</param>
    /// <returns>The formatted string resource as a <see cref="LocalizedString"/>.</returns>
    LocalizedString this[string nameparams object[] arguments] { get; }

    /// <summary>
    /// Gets all string resources.
    /// </summary>
    /// <param name="includeParentCultures">
    /// A <see cref="System.Boolean"/> indicating whether to include strings from parent cultures.
    /// </param>
    /// <returns>The strings.</returns>
    IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures);

Existe, además, una extensión a la interfaz, que expone el método GetString, que facilita aún más la obtención de valores

public static class StringLocalizerExtensions
{
    /// <summary>
    /// Gets the string resource with the given name.
    /// </summary>
    /// <param name="stringLocalizer">The <see cref="IStringLocalizer"/>.</param>
    /// <param name="name">The name of the string resource.</param>
    /// <returns>The string resource as a <see cref="LocalizedString"/>.</returns>
    public static LocalizedString GetString(
        this IStringLocalizer stringLocalizer,
        string name)
    {
        ArgumentNullThrowHelper.ThrowIfNull(stringLocalizer);
        ArgumentNullThrowHelper.ThrowIfNull(name);

        return stringLocalizer[name];
    }

Hagamos que nuestra clase mantenga en memoria aquellos textos que ya ha obtenido, preservándolos en un diccionario

private System.Collections.Concurrent.ConcurrentDictionary<stringstring> cachedElements
   = new();

Necesitaremos también preservar la cadena de conexión a la base de datos (que pasaremos al constructor) así como 2 objetos SqlCommand, uno para leer los valores y otro, que inserte aquellas claves que no encuentre para que resulte fácil saber que textos e idiomas nos están faltando

private readonly string ConnectionString;
SqlCommand command;
SqlCommand insertCommand;

En el constructor de la clase, aprovechemos para definir las sentencias T-SQL y los parámetros que corresponden a ambos objetos Command.

public Localizer(string connectionString)
{
   ConnectionString = connectionString;
   command = new SqlCommand("""
            SELECT [StringText]   
            FROM [Masters].[AppResources]
            WHERE  [Culture]=@Culture
            and [Key]=@Key
            """);
   command.Parameters.Add("@Culture", SqlDbType.NVarChar, 20);
   command.Parameters.Add("@Key", SqlDbType.NVarChar, 50);
   insertCommand = new SqlCommand("""
            INSERT INTO [Masters].[AppResources]
            ([Culture]
            ,[Key]
            ,[StringText]
            )
            VALUES
            (@Culture
            ,@Key
            ,'['+@Key+']')
 
            """);
   insertCommand.Parameters.Add("@Culture", SqlDbType.NVarChar, 20);
   insertCommand.Parameters.Add("@Key", SqlDbType.NVarChar, 50);
}

Definamos ahora una función que obtenga los textos, a la cual llamarán todos los métodos de implementación de la interfaz para centralizar la búsqueda en el diccionario en memoria y/o en base de datos.

private LocalizedString GetResultString(string key)
{
   if(string.IsNullOrWhiteSpace(key)) throw new ArgumentNullException(nameof(key));
   string value;
   string lookFor = $"{System.Threading.Thread.CurrentThread.CurrentCulture.Name}_{key}";
   if(cachedElements.ContainsKey(lookFor))
      value = cachedElements[lookFor];
   else
   {
      value = GetFromDb(key) ?? string.Empty;
      cachedElements.TryAdd(lookFor, value);
   }
   return new LocalizedString(key, value);
}

Las acciones realizadas con la base de datos, las implementamos en otra función, que realizará la lectura, así como la inserción de claves no encontradas.

private string? GetFromDb(string key)
{
   using SqlConnection con = new SqlConnection(ConnectionString);
   con.Open();
   command.Connection = con;
   CultureInfo culture = System.Threading.Thread.CurrentThread.CurrentCulture;
   command.Parameters["@Culture"].Value = culture.Name;
   command.Parameters["@Key"].Value = key;
   var result = command.ExecuteScalar();
   // If do not found the key for the current culture,
   // try to search the same for the parent culture,
   // which is more generic as in es-AR and es
   while(result is null)
   {
      culture = culture.Parent;
      command.Parameters["@Culture"].Value = culture.LCID == 127 ? " " : culture.Name;
      result = command.ExecuteScalar();
      if(culture.LCID == 127 && result is null//This is invariant culture. No text found
      {
         result = $"[{key}]";
         culture = System.Threading.Thread.CurrentThread.CurrentCulture;
         // if we match the Invariant culture with no results, means the key is not present in the table.
         // Must be inserted
         while(!culture.IsNeutralCulture)
            culture = culture.Parent;
         insertCommand.Connection = con;
         insertCommand.Parameters["@Culture"].Value = culture.Name;
         insertCommand.Parameters["@Key"].Value = key;
         insertCommand.ExecuteNonQuery();
         insertCommand.Parameters["@Culture"].Value = " ";
         insertCommand.ExecuteNonQuery();
      }
   }
   return $"{result}";
}

Finalmente, solo resta registrar como instancia única nuestra clase para la interfaz. Por ejemplo, en el archivo program.cs de una Aplicación ASP. Net Core

builder.Services.AddSingleton<IStringLocalizer>(
   provider => new Localizer(builder.Configuration.GetConnectionString("DefaultDatabase")));

Entonces, obtendremos la instancia de la clase por inyección de dependencias, tanto en código C#, como en páginas Blazor, etc.

Para que resulte más fácil de seguir, a continuación, la clase completa.

/// <summary>
/// Implements the <see cref="IStringLocalizer"/> interface,getting the values from a database table
/// </summary>
/// <seealso cref="Microsoft.Extensions.Localization.IStringLocalizer" />
/// <remarks>
/// Requires a table with at least the following schema
/// CREATE TABLE [Masters].[AppResources](
/// 	   [Culture] [nvarchar](20) NOT NULL,
///      [Key] [nvarchar] (50) NOT NULL,
///      [StringText] [nvarchar](max) NULL,
///  CONSTRAINT[PK_AppResources] PRIMARY KEY CLUSTERED
///   (
///      [Culture] ASC,
///      [Key] ASC
///   )
/// </remarks>
public class Localizer : IStringLocalizer
 
{
   private System.Collections.Concurrent.ConcurrentDictionary<stringstring> cachedElements
      = new();
   private readonly string ConnectionString;
   SqlCommand command;
   SqlCommand insertCommand;
   public Localizer(string connectionString)
   {
      ConnectionString = connectionString;
      command = new SqlCommand("""
               SELECT [StringText]   
               FROM [Masters].[AppResources]
               WHERE  [Culture]=@Culture
               and [Key]=@Key
               """);
      command.Parameters.Add("@Culture", SqlDbType.NVarChar, 20);
      command.Parameters.Add("@Key", SqlDbType.NVarChar, 50);
      insertCommand = new SqlCommand("""
               INSERT INTO [Masters].[AppResources]
               ([Culture]
               ,[Key]
               ,[StringText]
               )
               VALUES
               (@Culture
               ,@Key
               ,'['+@Key+']')
 
               """);
      insertCommand.Parameters.Add("@Culture", SqlDbType.NVarChar, 20);
      insertCommand.Parameters.Add("@Key", SqlDbType.NVarChar, 50);
   }
   public LocalizedString this[string name] { get => GetResultString(name); }
   public LocalizedString this[string nameparams object[] arguments] { get => GetResultString(name, arguments); }
 
   public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures=> throw new NotImplementedException();
   /// <summary>
   /// Gets the localized string for the <paramref name="key"/>
   /// </summary>
   /// <param name="key">The key to search.</param>
   /// <returns>The string for the current culture</returns>
   /// <exception cref="ArgumentNullException">nameof(key)</exception>
   private LocalizedString GetResultString(string key)
   {
      if(string.IsNullOrWhiteSpace(key)) throw new ArgumentNullException(nameof(key));
      string value;
      string lookFor = $"{System.Threading.Thread.CurrentThread.CurrentCulture.Name}_{key}";
      if(cachedElements.ContainsKey(lookFor))
         value = cachedElements[lookFor];
      else
      {
         value = GetFromDb(key) ?? string.Empty;
         cachedElements.TryAdd(lookFor, value);
      }
      return new LocalizedString(key, value);
   }
   /// <summary>
   ///  Gets the localized string for the <paramref name="key"/>
   /// </summary>
   /// <param name="key">The key.</param>
   /// <param name="arguments">The arguments.</param>
   /// <returns>The string for the current culture, with the <paramref name="arguments"/> included as values</returns>
   private LocalizedString GetResultString(string keyparams object[] arguments)
   {
      var resp = GetResultString(key);
      return new LocalizedString(resp.Name, string.Format(resp.Value, arguments));
   }
   private string? GetFromDb(string key)
   {
      using SqlConnection con = new SqlConnection(ConnectionString);
      con.Open();
      command.Connection = con;
      CultureInfo culture = System.Threading.Thread.CurrentThread.CurrentCulture;
      command.Parameters["@Culture"].Value = culture.Name;
      command.Parameters["@Key"].Value = key;
      var result = command.ExecuteScalar();
      // If do not found the key for the current culture,
      // try to search the same for the parent culture,
      // which is more generic as in es-AR and es
      while(result is null)
      {
         culture = culture.Parent;
         command.Parameters["@Culture"].Value = culture.LCID == 127 ? " " : culture.Name;
         result = command.ExecuteScalar();
         if(culture.LCID == 127 && result is null//This is invariant culture. No text found
         {
            result = $"[{key}]";
            culture = System.Threading.Thread.CurrentThread.CurrentCulture;
            // if we match the Invariant culture with no results, means the key is not present in the table.
            // Must be inserted
            while(!culture.IsNeutralCulture)
               culture = culture.Parent;
            insertCommand.Connection = con;
            insertCommand.Parameters["@Culture"].Value = culture.Name;
            insertCommand.Parameters["@Key"].Value = key;
            insertCommand.ExecuteNonQuery();
            insertCommand.Parameters["@Culture"].Value = " ";
            insertCommand.ExecuteNonQuery();
         }
      }
      return $"{result}";
   }
}
/// <summary>
/// /
/// </summary>
/// <typeparam name="T"></typeparam>
public class Localizer<T> : IStringLocalizer<T>
{
   IStringLocalizer _localizer;
 
   public Localizer(string connectionString)
   {
      _localizer = new Localizer(connectionString);
   }
   public LocalizedString this[string name] { get => _localizer[name]; }
   public LocalizedString this[string nameparams object[] arguments] { get => _localizer[name, arguments]; }
 
   public IEnumerable<LocalizedString> GetAllStrings(bool includeParentCultures=> _localizer.GetAllStrings();
}