asp.net-core
Localizzazione
Ricerca…
Localizzazione tramite risorse di linguaggio JSON
In ASP.NET Core ci sono diversi modi in cui possiamo localizzare / globalizzare la nostra app. È importante scegliere un modo adatto alle tue esigenze. In questo esempio vedremo come possiamo creare un'app di ASP.NET multilingue che legge stringhe specifiche della lingua da file .json
e li memorizza in memoria per fornire la localizzazione in tutte le sezioni dell'app e mantenere alte prestazioni.
Il modo in cui lo facciamo è utilizzando il pacchetto Microsoft.EntityFrameworkCore.InMemory
.
Gli appunti:
- Lo spazio dei nomi per questo progetto è
DigitalShop
che puoi modificare lo spazio dei nomi del tuo progetto - Prendi in considerazione la possibilità di creare un nuovo progetto in modo da non incorrere in strani errori
- In nessun modo questo esempio mostra le migliori pratiche, quindi se pensi che possa essere migliorato, ti preghiamo gentilmente di modificarlo
Per iniziare, aggiungiamo i seguenti pacchetti alla sezione delle dependencies
esistenti nel file project.json
:
"Microsoft.EntityFrameworkCore": "1.0.0",
"Microsoft.EntityFrameworkCore.SqlServer": "1.0.0",
"Microsoft.EntityFrameworkCore.InMemory": "1.0.0"
Ora cerchiamo di sostituire lo Startup.cs
file con: ( using
le dichiarazioni vengono rimossi in quanto possono essere facilmente aggiunti in seguito)
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));
}
}
}
Nel codice precedente, per prima cosa aggiungiamo tre variabili di campo public static
che verranno successivamente inizializzate utilizzando i valori letti dal file delle impostazioni.
Nel costruttore per la classe Startup
aggiungiamo un file di impostazioni json alla variabile builder
. Il primo file è necessario affinché l'app funzioni, quindi vai avanti e crea appsettings.json
nella root del progetto, se non esiste già. Utilizzando Visual Studio 2015, questo file viene creato automaticamente, quindi basta cambiarne il contenuto in: (Puoi omettere la sezione Logging
se non la usi)
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
}
}
Andando avanti, crea tre cartelle nella root del tuo progetto:
Models
, Services
e Languages
. Nella cartella Models
crea un'altra cartella denominata Localization
.
Nella cartella Services
creiamo un nuovo file .cs denominato EFLocalization
. Il contenuto sarebbe: (Anche in questo caso l' using
dichiarazioni non è incluso)
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;
}
}
}
Nel file sopra implementiamo l'interfaccia IStringLocalizerFactory
da Entity Framework Core per creare un servizio di localizzazione personalizzato. La parte importante è il costruttore di EFStringLocalizerFactory
cui creiamo un elenco di tutte le lingue disponibili e lo aggiungiamo al contesto del database. Ciascuno di questi file di lingua agisce come un database separato.
Ora aggiungi ognuno dei seguenti file alla cartella Models/Localization
:
Culture.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();
}
}
}
I file di cui sopra sono solo modelli che verranno popolati con risorse linguistiche, culture e c'è anche un tipico DBContext
utilizzato da EF Core.
L'ultima cosa di cui abbiamo bisogno per fare tutto questo è creare i file delle risorse linguistiche. I file JSON utilizzati per memorizzare una coppia chiave-valore per lingue diverse disponibili nella tua app.
In questo esempio la nostra app ha solo due lingue disponibili. Inglese e persiano. Per ognuna delle lingue abbiamo bisogno di due file. Un file JSON contenente coppie chiave-valore e un file .cs
che contiene una classe con lo stesso nome del file JSON. Quella classe ha un metodo, GetList
che deserializza il file JSON e lo restituisce. Questo metodo è chiamato nel costruttore di EFStringLocalizerFactory
che abbiamo creato in precedenza.
Quindi, crea questi quattro file nella cartella 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": "سلام"
},
]
Abbiamo finito. Ora per accedere alle stringhe della lingua (coppie chiave-valore) in qualsiasi punto del codice ( .cs
o .cshtml
) puoi fare quanto segue:
in un file .cs
(essere Controller o meno, non importa):
// Returns "Welcome" for en-US and "خوش آمدید" for fa-IR
var welcome = Startup._e["Welcome"];
in un file di visualizzazione Razor ( .cshtml
):
<h1>@Startup._e["Welcome"]</h1>
Poche cose da tenere a mente:
- Se si tenta di accedere a una
Key
che non esiste nel file JSON o caricata, si otterrà la chiave letterale (nell'esempio sopra, tentando di accedere aStartup._e["How are you"]
restituiràHow are you
non importa le impostazioni della lingua perché non esiste - Se si modifica un valore stringa in una lingua
.json
file, sarà necessario riavviare l'applicazione. Altrimenti mostrerà solo il valore predefinito (nome della chiave). Questo è particolarmente importante quando esegui la tua app senza eseguire il debug. - Il
appsettings.json
può essere utilizzato per memorizzare tutti i tipi di impostazioni di cui l'app potrebbe aver bisogno - Il riavvio dell'app non è necessario se si desidera semplicemente modificare le impostazioni di lingua / cultura dal file
appsettings.json
. Ciò significa che è possibile avere un'opzione nell'interfaccia delle app per consentire agli utenti di cambiare lingua / cultura in fase di runtime.
Ecco la struttura finale del progetto:
Imposta la cultura della richiesta tramite il percorso dell'URL
Per impostazione predefinita, il middleware di richiesta localizzazione integrato supporta solo l'impostazione della cultura tramite query, cookie o intestazione Accept-Language
. Questo esempio mostra come creare un middleware che consente di impostare la cultura come parte del percorso come in /api/en-US/products
.
Questo middleware di esempio presuppone che le impostazioni internazionali si trovino nel secondo segmento del percorso.
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));
}
}
Registrazione del 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);
Vincoli di percorso personalizzati
L'aggiunta e la creazione di vincoli di instradamento personalizzati sono mostrati nell'esempio Vincoli percorso . L'uso dei vincoli semplifica l'utilizzo dei vincoli del percorso personalizzato.
Registrazione del percorso
Esempio di registrazione dei percorsi senza l'utilizzo di vincoli personalizzati
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?}");
});