asp.net-core
локализация
Поиск…
Локализация с использованием языковых ресурсов JSON
В ASP.NET Core существует несколько различных способов локализации / глобализации нашего приложения. Важно выбрать способ, который соответствует вашим потребностям. В этом примере вы увидите, как мы можем сделать многоязычное приложение ASP.NET Core, которое считывает специфические для языка строки из .json
файлов и сохраняет их в памяти, чтобы обеспечить локализацию во всех разделах приложения, а также поддерживать высокую производительность.
Мы делаем это с помощью пакета Microsoft.EntityFrameworkCore.InMemory
.
Заметки:
- Пространство имен для этого проекта -
DigitalShop
которое вы можете изменить на свои собственные пространства имен проектов - Подумайте о создании нового проекта, чтобы вы не столкнулись с странными ошибками
- Ни в коем случае этот пример не показывает лучших практик, поэтому, если вы думаете, что его можно улучшить, пожалуйста, отредактируйте его
Для начала добавим следующие пакеты в существующий раздел dependencies
в файле project.json
:
"Microsoft.EntityFrameworkCore": "1.0.0",
"Microsoft.EntityFrameworkCore.SqlServer": "1.0.0",
"Microsoft.EntityFrameworkCore.InMemory": "1.0.0"
Теперь заменим файл Startup.cs
на: ( using
инструкции удаляются, поскольку их можно легко добавить позже)
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));
}
}
}
В приведенном выше коде мы сначала добавляем три public static
переменные public static
поля, которые мы позже будем инициализировать, используя значения, считанные из файла настроек.
В конструкторе класса Startup
мы добавляем файл настроек json в переменную builder
. Первый файл необходим для работы приложения, поэтому продолжайте и создайте appsettings.json
в корне вашего проекта, если он еще не существует. Используя Visual Studio 2015, этот файл создается автоматически, поэтому просто измените его содержимое на: (вы можете опустить раздел 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
}
}
Идти вперед, создайте три папки в корне проекта:
Models
, Services
и Languages
. В папке « Models
» создайте другую папку с именем « Localization
.
В папке « Services
» мы создаем новый .cs-файл с именем EFLocalization
. Содержимое будет выглядеть следующим образом: (Опять-таки using
операторов не включены)
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;
}
}
}
В вышеприведенном файле мы реализуем интерфейс IStringLocalizerFactory
из Entity Framework Core, чтобы создать пользовательскую службу локализатора. Важной частью является конструктор EFStringLocalizerFactory
где мы составляем список всех доступных языков и добавляем его в контекст базы данных. Каждый из этих языковых файлов действует как отдельная база данных.
Теперь добавьте каждый из следующих файлов в папку 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();
}
}
}
Вышеупомянутые файлы - это просто модели, которые будут заполнены языковыми ресурсами, культурами, а также типичный DBContext
используемый EF Core.
Последнее, что нам нужно сделать для всей этой работы, - создать файлы языковых ресурсов. Файлы JSON, используемые для хранения пары ключ-значение для разных языков, доступных в вашем приложении.
В этом примере наше приложение имеет только два доступных языка. Английский и персидский. Для каждого из языков нам нужны два файла. Файл JSON, содержащий пары ключ-значение и файл .cs
который содержит класс с тем же именем, что и файл JSON. Этот класс имеет один метод GetList
который десериализует JSON-файл и возвращает его. Этот метод вызывается в конструкторе EFStringLocalizerFactory
который мы создали ранее.
Итак, создайте эти четыре файла в папке « Languages
»:
ан-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);
}
}
}
ан-US.json
[
{
"Key": "Welcome",
"Value": "Welcome"
},
{
"Key": "Hello",
"Value": "Hello"
},
]
к.-а.-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);
}
}
фа-IR.json
[
{
"Key": "Welcome",
"Value": "خوش آمدید"
},
{
"Key": "Hello",
"Value": "سلام"
},
]
Мы все закончили. Теперь, чтобы получить доступ к языковым строкам (пары ключ-значение) в любом месте вашего кода ( .cs
или .cshtml
), вы можете сделать следующее:
в файле .cs
(быть контроллером или нет, не имеет значения):
// Returns "Welcome" for en-US and "خوش آمدید" for fa-IR
var welcome = Startup._e["Welcome"];
в файле вида Razor ( .cshtml
):
<h1>@Startup._e["Welcome"]</h1>
Мало что нужно помнить:
- Если вы попытаетесь получить доступ к
Key
, который не существует в файле JSON или загружен, вы просто получите ключевой литерал (в приведенном выше примере попытка получить доступ кStartup._e["How are you"]
вернется.How are you
независимо от настроек языка, поскольку он не существует - Если изменить значение строки в языке
.json
файла, вам нужно будет перезапустить приложение. В противном случае он просто покажет значение по умолчанию (имя ключа). Это особенно важно при запуске приложения без отладки. - Приложение
appsettings.json
можно использовать для хранения всех параметров, которые могут понадобиться вашему приложению. - Перезапуск приложения не требуется, если вы просто хотите изменить настройки языка / культуры из файла
appsettings.json
. Это означает, что у вас может быть опция в вашем интерфейсе приложений, чтобы пользователи могли изменять язык / культуру во время выполнения.
Вот окончательная структура проекта:
Задать культуру запросов через URL-адрес
По умолчанию встроенное промежуточное ПО для локализации запроса поддерживает настройку культуры через заголовок запроса, cookie или Accept-Language
. В этом примере показано, как создать промежуточное программное обеспечение, которое позволяет установить культуру как часть пути, например, в /api/en-US/products
.
В этом примере промежуточное ПО предполагает, что локаль находится во втором сегменте пути.
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));
}
}
Регистрация промежуточного ПО
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);
Пользовательские ограничения маршрута
Добавление и создание ограничений пользовательского маршрута показаны в примере ограничений ограничений маршрута . Использование ограничений упрощает использование пользовательских ограничений маршрута.
Регистрация маршрута
Пример регистрации маршрутов без использования пользовательских ограничений
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?}");
});