Поиск…


Локализация с использованием языковых ресурсов JSON

В ASP.NET Core существует несколько различных способов локализации / глобализации нашего приложения. Важно выбрать способ, который соответствует вашим потребностям. В этом примере вы увидите, как мы можем сделать многоязычное приложение ASP.NET Core, которое считывает специфические для языка строки из .json файлов и сохраняет их в памяти, чтобы обеспечить локализацию во всех разделах приложения, а также поддерживать высокую производительность.

Мы делаем это с помощью пакета Microsoft.EntityFrameworkCore.InMemory .

Заметки:

  1. Пространство имен для этого проекта - DigitalShop которое вы можете изменить на свои собственные пространства имен проектов
  2. Подумайте о создании нового проекта, чтобы вы не столкнулись с странными ошибками
  3. Ни в коем случае этот пример не показывает лучших практик, поэтому, если вы думаете, что его можно улучшить, пожалуйста, отредактируйте его

Для начала добавим следующие пакеты в существующий раздел 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?}"); 
});


Modified text is an extract of the original Stack Overflow Documentation
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow