asp.net-core
Lokalizacja
Szukaj…
Lokalizacja z wykorzystaniem zasobów językowych JSON
W ASP.NET Core istnieje kilka różnych sposobów lokalizacji / globalizacji naszej aplikacji. Ważne jest, aby wybrać sposób, który odpowiada Twoim potrzebom. W tym przykładzie zobaczysz, jak możemy stworzyć wielojęzyczną aplikację ASP.NET Core, która odczytuje ciągi znaków specyficzne dla języka z plików .json
i przechowuje je w pamięci, aby zapewnić lokalizację we wszystkich sekcjach aplikacji, a także utrzymać wysoką wydajność.
Robimy to za pomocą pakietu Microsoft.EntityFrameworkCore.InMemory
.
Uwagi:
- Przestrzeń nazw dla tego projektu to
DigitalShop
, którą możesz zmienić na własną przestrzeń nazw swoich projektów - Zastanów się nad utworzeniem nowego projektu, aby nie popaść w dziwne błędy
- W żadnym wypadku ten przykład nie pokazuje najlepszych praktyk, więc jeśli uważasz, że można go poprawić, prosimy o jego edycję
Aby rozpocząć, dodajmy następujące pakiety do istniejącej sekcji dependencies
w pliku project.json
:
"Microsoft.EntityFrameworkCore": "1.0.0",
"Microsoft.EntityFrameworkCore.SqlServer": "1.0.0",
"Microsoft.EntityFrameworkCore.InMemory": "1.0.0"
Teraz Startup.cs
plik Startup.cs
: ( using
instrukcji są usuwane, ponieważ można je później łatwo dodać)
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));
}
}
}
W powyższym kodzie najpierw dodajemy trzy public static
zmienne pola public static
, które później zainicjujemy przy użyciu wartości odczytanych z pliku ustawień.
W konstruktorze klasy Startup
dodajemy plik ustawień json do zmiennej builder
. Pierwszy plik jest wymagany do działania aplikacji, więc stwórz plik appsettings.json
w katalogu głównym projektu, jeśli jeszcze nie istnieje. Za pomocą programu Visual Studio 2015 ten plik jest tworzony automatycznie, więc po prostu zmień jego zawartość na: (Możesz pominąć sekcję Logging
jeśli go nie używasz)
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
}
}
Idąc dalej, utwórz trzy foldery w katalogu głównym projektu:
Models
, Services
i Languages
. W folderze Models
utwórz kolejny folder o nazwie Localization
.
W folderze Services
tworzymy nowy plik .cs o nazwie EFLocalization
. Treść będzie: (Ponownie using
instrukcji nie jest uwzględnionych)
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;
}
}
}
W powyższym pliku implementujemy interfejs IStringLocalizerFactory
z Entity Framework Core w celu stworzenia niestandardowej usługi lokalizatora. Ważną częścią jest konstruktor EFStringLocalizerFactory
którym tworzymy listę wszystkich dostępnych języków i dodajemy ją do kontekstu bazy danych. Każdy z tych plików językowych działa jako osobna baza danych.
Teraz dodaj każdy z następujących plików do folderu 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();
}
}
}
Powyższe pliki to tylko modele, które zostaną wypełnione zasobami językowymi, kulturami, a także typowy DBContext
używany przez EF Core.
Ostatnią rzeczą, którą musimy zrobić, aby wszystko to działało, jest utworzenie plików zasobów językowych. Pliki JSON używane do przechowywania pary klucz-wartość dla różnych języków dostępnych w Twojej aplikacji.
W tym przykładzie nasza aplikacja ma dostępne tylko dwa języki. Angielski i perski. Dla każdego z języków potrzebujemy dwóch plików. Plik JSON zawierający pary klucz-wartość i plik .cs
, który zawiera klasę o tej samej nazwie co plik JSON. Ta klasa ma jedną metodę GetList
która deserializuje plik JSON i zwraca go. Ta metoda jest wywoływana w konstruktorze EFStringLocalizerFactory
, który utworzyliśmy wcześniej.
Utwórz więc te cztery pliki w folderze 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": "سلام"
},
]
Wszyscy jesteśmy skończeni. Teraz, aby uzyskać dostęp do ciągów językowych (par klucz-wartość) w dowolnym miejscu w kodzie ( .cs
lub .cshtml
), możesz wykonać następujące czynności:
w pliku .cs
(kontroler lub nie, nie ma znaczenia):
// Returns "Welcome" for en-US and "خوش آمدید" for fa-IR
var welcome = Startup._e["Welcome"];
w pliku widoku Razor ( .cshtml
):
<h1>@Startup._e["Welcome"]</h1>
Kilka rzeczy, o których warto pamiętać:
- Jeśli spróbujesz uzyskać dostęp do
Key
, który nie istnieje w pliku JSON lub nie zostanie załadowany, otrzymasz literał klucza (w powyższym przykładzie próba uzyskania dostępu doStartup._e["How are you"]
zwróciHow are you
bez względu na ustawienia języka, ponieważ on nie istnieje - W przypadku zmiany wartości ciągu w języku
.json
pliku, trzeba będzie ponownie uruchomić aplikację. W przeciwnym razie pokaże tylko wartość domyślną (nazwę klucza). Jest to szczególnie ważne, gdy uruchamiasz aplikację bez debugowania. - Plik
appsettings.json
może służyć do przechowywania wszelkiego rodzaju ustawień, które mogą być potrzebne Twojej aplikacji - Ponowne uruchomienie aplikacji nie jest konieczne, jeśli chcesz tylko zmienić ustawienia języka / kultury z pliku
appsettings.json
. Oznacza to, że możesz mieć opcję w interfejsie aplikacji, aby umożliwić użytkownikom zmianę języka / kultury w czasie wykonywania.
Oto ostateczna struktura projektu:
Ustaw kulturę żądania za pomocą ścieżki adresu URL
Domyślnie wbudowane oprogramowanie pośrednie lokalizacji żądań obsługuje ustawianie kultury tylko za pomocą zapytania, pliku cookie lub nagłówka Accept-Language
. Ten przykład pokazuje, jak utworzyć oprogramowanie pośrednie, które pozwala ustawić kulturę jako część ścieżki, jak w /api/en-US/products
.
W tym przykładzie oprogramowanie pośrednie zakłada, że ustawienia regionalne znajdują się w drugim segmencie ścieżki.
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));
}
}
Rejestracja oprogramowania pośredniego
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);
Niestandardowe ograniczenia trasy
Dodawanie i tworzenie niestandardowych ograniczeń trasy pokazano w przykładzie Ograniczenia trasy . Korzystanie z ograniczeń upraszcza korzystanie z niestandardowych ograniczeń trasy.
Rejestracja trasy
Przykład rejestracji tras bez użycia niestandardowych ograniczeń
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?}");
});