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:

  1. Przestrzeń nazw dla tego projektu to DigitalShop , którą możesz zmienić na własną przestrzeń nazw swoich projektów
  2. Zastanów się nad utworzeniem nowego projektu, aby nie popaść w dziwne błędy
  3. 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 do Startup._e["How are you"] zwróci How 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:

wprowadź opis zdjęcia tutaj

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?}"); 
});


Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow