asp.net-core
Localización
Buscar..
Localización utilizando recursos de lenguaje JSON
En ASP.NET Core hay varias maneras diferentes en que podemos localizar / globalizar nuestra aplicación. Es importante elegir una manera que se adapte a sus necesidades. En este ejemplo, verá cómo podemos crear una aplicación de ASP.NET Core multilingüe que lea cadenas específicas del .json
archivos .json
y almacenarlas en la memoria para proporcionar localización en todas las secciones de la aplicación, así como mantener un alto rendimiento.
La forma en que lo hacemos es mediante el uso del paquete Microsoft.EntityFrameworkCore.InMemory
.
Notas:
- El espacio de nombres para este proyecto es
DigitalShop
que puede cambiar al espacio de nombres propio de sus proyectos. - Considera crear un nuevo proyecto para que no te encuentres con errores extraños
- De ninguna manera este ejemplo muestra las mejores prácticas, así que si cree que puede mejorarse, por favor edítelo.
Para comenzar, agreguemos los siguientes paquetes a la sección de dependencies
existentes en el archivo project.json
:
"Microsoft.EntityFrameworkCore": "1.0.0",
"Microsoft.EntityFrameworkCore.SqlServer": "1.0.0",
"Microsoft.EntityFrameworkCore.InMemory": "1.0.0"
Ahora reemplacemos el archivo Startup.cs
con: ( using
declaraciones se eliminan, ya que se pueden agregar fácilmente más adelante)
Startup.cs
namespace DigitalShop
{
public class Startup
{
public static string UiCulture;
public static string CultureDirection;
public static IStringLocalizer _e; // This is how we access language strings
public static IConfiguration LocalConfig;
public Startup(IHostingEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) // this is where we store apps configuration including language
.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
.AddEnvironmentVariables();
Configuration = builder.Build();
LocalConfig = Configuration;
}
public IConfigurationRoot Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().AddViewLocalization().AddDataAnnotationsLocalization();
// IoC Container
// Add application services.
services.AddTransient<EFStringLocalizerFactory>();
services.AddSingleton<IConfiguration>(Configuration);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, EFStringLocalizerFactory localizerFactory)
{
_e = localizerFactory.Create(null);
// a list of all available languages
var supportedCultures = new List<CultureInfo>
{
new CultureInfo("en-US"),
new CultureInfo("fa-IR")
};
var requestLocalizationOptions = new RequestLocalizationOptions
{
SupportedCultures = supportedCultures,
SupportedUICultures = supportedCultures,
};
requestLocalizationOptions.RequestCultureProviders.Insert(0, new JsonRequestCultureProvider());
app.UseRequestLocalization(requestLocalizationOptions);
app.UseStaticFiles();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
public class JsonRequestCultureProvider : RequestCultureProvider
{
public override Task<ProviderCultureResult> DetermineProviderCultureResult(HttpContext httpContext)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
var config = Startup.LocalConfig;
string culture = config["AppOptions:Culture"];
string uiCulture = config["AppOptions:UICulture"];
string culturedirection = config["AppOptions:CultureDirection"];
culture = culture ?? "fa-IR"; // Use the value defined in config files or the default value
uiCulture = uiCulture ?? culture;
Startup.UiCulture = uiCulture;
culturedirection = culturedirection ?? "rlt"; // rtl is set to be the default value in case culturedirection is null
Startup.CultureDirection = culturedirection;
return Task.FromResult(new ProviderCultureResult(culture, uiCulture));
}
}
}
En el código anterior, primero agregamos tres variables public static
campo public static
que luego inicializaremos usando los valores leídos en el archivo de configuración.
En el constructor para la clase de Startup
, agregamos un archivo de configuración json a la variable del builder
. El primer archivo es necesario para que la aplicación funcione, así que adelante, cree appsettings.json
en la raíz de su proyecto si aún no existe. Usando Visual Studio 2015, este archivo se crea automáticamente, así que solo cambie su contenido a: (Si no lo usa, puede omitir la sección de Logging
)
appsettings.json
{
"Logging": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information"
}
},
"AppOptions": {
"Culture": "en-US", // fa-IR for Persian
"UICulture": "en-US", // same as above
"CultureDirection": "ltr" // rtl for Persian/Arabic/Hebrew
}
}
En el futuro, crea tres carpetas en la raíz de tu proyecto:
Models
, Services
e Languages
. En la carpeta Models
cree otra carpeta llamada Localization
.
En la carpeta Services
, creamos un nuevo archivo .cs llamado EFLocalization
. El contenido sería: (Nuevamente using
declaraciones no se incluyen)
EFLocalization.cs
namespace DigitalShop.Services
{
public class EFStringLocalizerFactory : IStringLocalizerFactory
{
private readonly LocalizationDbContext _db;
public EFStringLocalizerFactory()
{
_db = new LocalizationDbContext();
// Here we define all available languages to the app
// available languages are those that have a json and cs file in
// the Languages folder
_db.AddRange(
new Culture
{
Name = "en-US",
Resources = en_US.GetList()
},
new Culture
{
Name = "fa-IR",
Resources = fa_IR.GetList()
}
);
_db.SaveChanges();
}
public IStringLocalizer Create(Type resourceSource)
{
return new EFStringLocalizer(_db);
}
public IStringLocalizer Create(string baseName, string location)
{
return new EFStringLocalizer(_db);
}
}
public class EFStringLocalizer : IStringLocalizer
{
private readonly LocalizationDbContext _db;
public EFStringLocalizer(LocalizationDbContext db)
{
_db = db;
}
public LocalizedString this[string name]
{
get
{
var value = GetString(name);
return new LocalizedString(name, value ?? name, resourceNotFound: value == null);
}
}
public LocalizedString this[string name, params object[] arguments]
{
get
{
var format = GetString(name);
var value = string.Format(format ?? name, arguments);
return new LocalizedString(name, value, resourceNotFound: format == null);
}
}
public IStringLocalizer WithCulture(CultureInfo culture)
{
CultureInfo.DefaultThreadCurrentCulture = culture;
return new EFStringLocalizer(_db);
}
public IEnumerable<LocalizedString> GetAllStrings(bool includeAncestorCultures)
{
return _db.Resources
.Include(r => r.Culture)
.Where(r => r.Culture.Name == CultureInfo.CurrentCulture.Name)
.Select(r => new LocalizedString(r.Key, r.Value, true));
}
private string GetString(string name)
{
return _db.Resources
.Include(r => r.Culture)
.Where(r => r.Culture.Name == CultureInfo.CurrentCulture.Name)
.FirstOrDefault(r => r.Key == name)?.Value;
}
}
public class EFStringLocalizer<T> : IStringLocalizer<T>
{
private readonly LocalizationDbContext _db;
public EFStringLocalizer(LocalizationDbContext db)
{
_db = db;
}
public LocalizedString this[string name]
{
get
{
var value = GetString(name);
return new LocalizedString(name, value ?? name, resourceNotFound: value == null);
}
}
public LocalizedString this[string name, params object[] arguments]
{
get
{
var format = GetString(name);
var value = string.Format(format ?? name, arguments);
return new LocalizedString(name, value, resourceNotFound: format == null);
}
}
public IStringLocalizer WithCulture(CultureInfo culture)
{
CultureInfo.DefaultThreadCurrentCulture = culture;
return new EFStringLocalizer(_db);
}
public IEnumerable<LocalizedString> GetAllStrings(bool includeAncestorCultures)
{
return _db.Resources
.Include(r => r.Culture)
.Where(r => r.Culture.Name == CultureInfo.CurrentCulture.Name)
.Select(r => new LocalizedString(r.Key, r.Value, true));
}
private string GetString(string name)
{
return _db.Resources
.Include(r => r.Culture)
.Where(r => r.Culture.Name == CultureInfo.CurrentCulture.Name)
.FirstOrDefault(r => r.Key == name)?.Value;
}
}
}
En el archivo anterior implementamos la interfaz IStringLocalizerFactory
desde Entity Framework Core para poder crear un servicio de localizador personalizado. La parte importante es el constructor de EFStringLocalizerFactory
donde hacemos una lista de todos los idiomas disponibles y lo agregamos al contexto de la base de datos. Cada uno de estos archivos de idioma actúa como una base de datos separada.
Ahora agregue cada uno de los siguientes archivos a la carpeta Models/Localization
:
Cultura.cs
namespace DigitalShop.Models.Localization
{
public class Culture
{
public int Id { get; set; }
public string Name { get; set; }
public virtual List<Resource> Resources { get; set; }
}
}
Resource.cs
namespace DigitalShop.Models.Localization
{
public class Resource
{
public int Id { get; set; }
public string Key { get; set; }
public string Value { get; set; }
public virtual Culture Culture { get; set; }
}
}
LocalizationDbContext.cs
namespace DigitalShop.Models.Localization
{
public class LocalizationDbContext : DbContext
{
public DbSet<Culture> Cultures { get; set; }
public DbSet<Resource> Resources { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UseInMemoryDatabase();
}
}
}
Los archivos anteriores son solo modelos que se rellenarán con recursos lingüísticos, culturas y también hay un DBContext
típico utilizado por EF Core.
Lo último que necesitamos para hacer que todo este trabajo sea crear los archivos de recursos de idioma. Los archivos JSON utilizados para almacenar un par clave-valor para diferentes idiomas disponibles en su aplicación.
En este ejemplo, nuestra aplicación solo tiene dos idiomas disponibles. Inglés y persa. Para cada uno de los idiomas necesitamos dos archivos. Un archivo JSON que contiene pares clave-valor y un archivo .cs
que contiene una clase con el mismo nombre que el archivo JSON. Esa clase tiene un método, GetList
que deserializa el archivo JSON y lo devuelve. Este método se llama en el constructor de EFStringLocalizerFactory
que creamos anteriormente.
Entonces, crea estos cuatro archivos en tu carpeta de Languages
:
en-US.cs
namespace DigitalShop.Languages
{
public static class en_US
{
public static List<Resource> GetList()
{
var jsonSerializerSettings = new JsonSerializerSettings();
jsonSerializerSettings.MissingMemberHandling = MissingMemberHandling.Ignore;
return JsonConvert.DeserializeObject<List<Resource>>(File.ReadAllText("Languages/en-US.json"), jsonSerializerSettings);
}
}
}
en-US.json
[
{
"Key": "Welcome",
"Value": "Welcome"
},
{
"Key": "Hello",
"Value": "Hello"
},
]
fa-IR.cs
public static class fa_IR
{
public static List<Resource> GetList()
{
var jsonSerializerSettings = new JsonSerializerSettings();
jsonSerializerSettings.MissingMemberHandling = MissingMemberHandling.Ignore;
return JsonConvert.DeserializeObject<List<Resource>>(File.ReadAllText("Languages/fa-IR.json", Encoding.UTF8), jsonSerializerSettings);
}
}
fa-IR.json
[
{
"Key": "Welcome",
"Value": "خوش آمدید"
},
{
"Key": "Hello",
"Value": "سلام"
},
]
Todos hemos terminado. Ahora, para acceder a las cadenas de idioma (pares clave-valor) en cualquier parte de su código ( .cs
o .cshtml
) puede hacer lo siguiente:
en un archivo .cs
(sea controlador o no, no importa):
// Returns "Welcome" for en-US and "خوش آمدید" for fa-IR
var welcome = Startup._e["Welcome"];
en un archivo de vista de Razor ( .cshtml
):
<h1>@Startup._e["Welcome"]</h1>
Pocas cosas a tener en cuenta:
- Si intenta acceder a una
Key
que no existe en el archivo JSON o está cargada, solo obtendrá la clave literal (en el ejemplo anterior, al intentar acceder aStartup._e["How are you"]
volveráHow are you
No importa la configuración de idioma porque no existe - Si cambia el valor de una cadena en un archivo
.json
idioma, deberá REINICIAR la aplicación. De lo contrario, solo mostrará el valor predeterminado (nombre de clave). Esto es especialmente importante cuando está ejecutando su aplicación sin depurar. -
appsettings.json
puede usarse para almacenar todo tipo de configuraciones que su aplicación pueda necesitar. - No es necesario reiniciar la aplicación si solo desea cambiar la configuración de idioma / cultura del archivo
appsettings.json
. Esto significa que puede tener una opción en la interfaz de su aplicación para permitir a los usuarios cambiar el idioma / cultura en tiempo de ejecución.
Aquí está la estructura final del proyecto:
Establecer cultura de solicitud a través de la ruta url
De forma predeterminada, el middleware de localización de solicitudes incorporado solo admite la configuración de la cultura mediante consulta, cookie o encabezado Accept-Language
. Este ejemplo muestra cómo crear un middleware que permita establecer la cultura como parte de la ruta como en /api/en-US/products
.
Este middleware de ejemplo asume que la configuración regional está en el segundo segmento de la ruta.
public class UrlRequestCultureProvider : RequestCultureProvider
{
private static readonly Regex LocalePattern = new Regex(@"^[a-z]{2}(-[a-z]{2,4})?$",
RegexOptions.IgnoreCase);
public override Task<ProviderCultureResult> DetermineProviderCultureResult(HttpContext httpContext)
{
if (httpContext == null)
{
throw new ArgumentNullException(nameof(httpContext));
}
var url = httpContext.Request.Path;
// Right now it's not possible to use httpContext.GetRouteData()
// since it uses IRoutingFeature placed in httpContext.Features when
// Routing Middleware registers. It's not set when the Localization Middleware
// is called, so this example simply assumes the locale will always
// be located in the second segment of a path, like in /api/en-US/products
var parts = httpContext.Request.Path.Value.Split('/');
if (parts.Length < 3)
{
return Task.FromResult<ProviderCultureResult>(null);
}
if (!LocalePattern.IsMatch(parts[2]))
{
return Task.FromResult<ProviderCultureResult>(null);
}
var culture = parts[2];
return Task.FromResult(new ProviderCultureResult(culture));
}
}
Registro de middleware
var localizationOptions = new RequestLocalizationOptions
{
SupportedCultures = new List<CultureInfo>
{
new CultureInfo("de-DE"),
new CultureInfo("en-US"),
new CultureInfo("en-GB")
},
SupportedUICultures = new List<CultureInfo>
{
new CultureInfo("de-DE"),
new CultureInfo("en-US"),
new CultureInfo("en-GB")
},
DefaultRequestCulture = new RequestCulture("en-US")
};
// Adding our UrlRequestCultureProvider as first object in the list
localizationOptions.RequestCultureProviders.Insert(0, new UrlRequestCultureProvider
{
Options = localizationOptions
});
app.UseRequestLocalization(localizationOptions);
Restricciones de ruta personalizadas
Agregar y crear restricciones de ruta personalizadas se muestran en el ejemplo Restricciones de ruta . El uso de restricciones simplifica el uso de restricciones de ruta personalizadas.
Registro de la ruta
Ejemplo de registro de rutas sin utilizar restricciones personalizadas.
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "api/{culture::regex(^[a-z]{{2}}-[A-Za-z]{{4}}$)}}/{controller}/{id?}");
routes.MapRoute(
name: "default",
template: "api/{controller}/{id?}");
});