Wstęp

To jest kolejny artykuł z serii o globalizacji i lokalizacji. Jeśli nie czytałeś poprzednich, koniecznie to nadrób. W tym artykule opisuję TYLKO jak ładować tłumaczenia w aplikacjach internetowych tworzonych w RAZOR. Poprzedni artykuł – Tłumaczenie aplikacji cz. 3 – jak to ogarnąć? – daje całą podstawę.

W aplikacjach internetowych możemy uwzględniać język na kilka sposobów:

  • informacji wysyłanej z przeglądarki (nagłówek żądania)
  • parametru w zapytaniu (np. https://example.com?lang=en)
  • ciasteczka
  • fragmentu URL (np. https://example.com/en-US/)

Popatrzymy na te wszystkie możliwości.

Żeby w ogóle cała machina ruszyła, trzeba skonfigurować lokalizację… To naprawdę proste, wystarczy zrozumieć 🙂

Czym jest middleware pipeline?

Jeśli wiesz, czym jest middleware pipeline w .NetCore, możesz przejść dalej. Jeśli nie wiesz – też możesz, ale dalsza część artykułu będzie trochę niejasna.

Napiszę bardzo ogólnie jak to działa, dużo lepiej jest to opisane w artykule o middleware pipeline 🙂

Pipeline (czyli potok) to seria akcji wykonywanych jedna po drugiej podczas odbierania żądania od klienta i wysyłania odpowiedzi. W metodzie Configure ustawiasz właśnie te komponenty w pipelinie za pomocą metod, których nazwy rozpoczynają się zwyczajowo od Use. Np. UseAuthentication, UseAuthorization itd. Spójrz na przykładowe kody:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
   app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
}

Dla takiego kodu pipeline będzie prawie pusty:

Dodajmy teraz przekierowanie https:

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
   app.UseHttpsRedirection();
   app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");
}

Teraz pipeline będzie wyglądał tak:

Żądanie przejdzie najpierw przez HttpsRedirection, który może sobie na nim pracować i może przekazać wywołanie do kolejnego middleware (ale wcale nie musi). Żądanie może następnie trafić do RouterMiddleware, który wie, jaką stronę ma pokazać. Następnie generowana jest odpowiedź, która przechodzi przez middleware’y w odwrotnej kolejności (w tym momencie nie można już zmodyfikować nagłówków).

Widzisz zatem, że kolejność dodawania middleware’ów do pipeline jest istotna, a nawet dokładnie opisana na stronie Microsoftu: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-6.0#middleware-order . No bo co się stanie, jeśli dodasz autoryzację za routerem? Autoryzacja zadziała dopiero za routerem, a więc użytkownik zobaczy już stronę, której nie powinien.

Konfiguracja języków

Najpierw trzeba skonfigurować języki w aplikacji RAZOR. Przede wszystkim zajrzyj do pliku Startup.cs i tam odnajdź metodę ConfigureServices. (jeśli używasz .NET6, możesz nie widzieć Startup.cs, wszystko dzieje się w pliku Program.cs)

Teraz musisz w niej skonfigurować serwis odpowiedzialny za lokalizację. Są takie metody (extensions) w IServiceCollection jak AddControllers*, AddMVC*, czy też AddRazorPages. Każda z nich zwraca obiekt implementujący IMvcBuilder. Z kolei ten, ma w sobie rejestrację lokalizacji (AddViewLocalization()), a więc np:

using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.DependencyInjection;

//...
public void ConfigureServices(IServiceCollection services)
{
  services.AddControllersWithViews()
      .AddViewLocalization();
}

Najprostszą konfigurację lokalizacji robimy w metodzie Configure – PRZED mapowaniem ścieżek. A więc dodajemy to do pipeline. Wygląda to tak:

IList<CultureInfo> supportedCultures = new List<CultureInfo>
{
    new CultureInfo("en-US"),
    new CultureInfo("pl"),
};

var localizationOptions = new RequestLocalizationOptions
{
    DefaultRequestCulture = new RequestCulture("en-US"),
    SupportedCultures = supportedCultures,
    SupportedUICultures = supportedCultures        
};

app.UseRequestLocalization(localizationOptions);

Teraz przyda się kilka słów wyjaśnienia.

Najpierw trzeba użyć oprogramowania pośredniczącego (middleware) do lokalizacji. Robimy to przez włączenie do pipeline UseRequestLocalization. Można to zrobić na kilka sposobów:

  • app.UseRequestLocalization() – bez parametrów – odczyta lokalizację z nagłówka żądania, który wysyłany jest przez przeglądarkę. I tyle. Niczego tu nie można zmienić.
  • app.UseRequestLocalization(RequestLocalizationOptions) – od razu skonfiguruje middleware RequestLocalization zgodnie z przekazanymi opcjami
  • app.UseRequestLocalization(Action) – podobnie jak wyżej, tyle że przekazujemy tutaj akcję, w której konfigurujemy middleware.

W naszym przykładzie włączamy RequestLocalization do pipeline (pamiętaj, że ZANIM zmapujemy ścieżki), przekazując opcje.

Wróćmy do kodu:

IList<CultureInfo> supportedCultures = new List<CultureInfo>
{
    new CultureInfo("en-US"),
    new CultureInfo("pl"),
};

var localizationOptions = new RequestLocalizationOptions
{
    DefaultRequestCulture = new RequestCulture("en-US"),
    SupportedCultures = supportedCultures,
    SupportedUICultures = supportedCultures        
};

app.UseRequestLocalization(localizationOptions);

Najpierw tworzona jest lista kultur, które wspieramy, a w drugim kroku ustawiamy opcje lokalizacji:

  • przekazujemy domyślną kulturę (DefaultRequestCulture)
  • przypisujemy wspierane kultury UI i zwykłe

Taka konfiguracja daje nam dostęp do:

  • odczytu lokalizacji z przeglądarki (z nagłówka żądania)
  • odczytu lokalizacji z parametrów zapytania (?culture=pl-PL)
  • odczytu lokalizacji z ciasteczka

Czyli konfigurując w taki sposób (z przekazaniem RequestLocalizationOptions) mamy dużo więcej niż po prostu włączając middleware do pipeline bez jego konfiguracji.

To teraz pytanie, skąd system wie, w jaki sposób ma pobrać dane o aktualnej kulturze? Czary? Nie! Z pomocą przychodzi…

RequestCultureProvider

To jest klasa abstrakcyjna, której zadaniem jest zwrócić informacje o kulturze na podstawie danych z żądania. Kilka domyślnych providerów jest już utworzonych i właściwie nie potrzeba więcej, chociaż możesz stworzyć własne (np. odczyt kultury z bazy danych).

W klasie RequestLocalizationOptions (opcje lokalizacyjne) poza obsługiwanymi kulturami znajduje się też lista RequestCultureProvider. Domyślnie utworzone są takie:

QueryStringRequestCultureProvider

zwraca kulturę z zapytania w adresie, np: https://example.com/Home/Index?culture=en-US; świetnie nadaje się to do debugowania. Domyślnie operuje na dwóch kluczach: culture i ui-culture. Wystarczy, że w zapytaniu będzie jeden z nich, drugi otrzyma taką samą wartość. Jeśli są oba, np: ?culture=en-US&ui-culture=en-GB, wtedy inne będą ustawienia dla CurrentCulture i CurrentUICulture.

Oczywiście klucze możesz sobie zmieniać za pomocą właściwości

  • QueryStringKey (domyślnie „culture”)
  • UIQueryStringKey (domyślnie „ui-culture”)

Także zamiast ?culture=en-US będziesz mógł podać np. ?lang=en

CookieRequestCultureProvider

zwraca kulturę z ciasteczka. Sam możesz zdecydować o tym, jak ma nazywać się dane ciasteczko (za pomocą właściwości CookieName). Domyślnie to: „.AspNetCore.Culture”.

Żeby to zadziałało, oczywiście jakieś ciasteczko musi zostać wcześniej zapisane. Ta klasa ma dwie przydatne metody statyczne: ParseCookieValue i MakeCookieValue. MakeCookieValue zwróci Ci dokładną zawartość ciasteczka, jakie musisz zapisać.

AcceptLanguageHeaderRequestCultureProvider

zwraca kulturę zapisaną w przeglądarce (a właściwie wysłaną przez przeglądarkę w nagłówkach).

Kolejność tych providerów jest istotna. Jeśli pierwszy nie zwróci danych, drugi spróbuje. Jeśli w przeglądarce masz zapisaną kulturę pl-PL, ale w zapytaniu w adresie strony wpiszesz ?culture=en-US, zobaczysz stronę po angielsku, ponieważ pierwszy w kolejności jest QueryStringRequestCultureProvider.

Oczywiście manipulując tą listą możesz zmienić kolejność providerów, usuwać ich i dodawać nowych.

Pobieranie języka z adresu

Pewnie nie raz widziałeś (chociażby na stronach Microsoftu) taki sposób przekazywania kultury: https://example.com/en-US/Home/Index

gdzie informacje o niej są zawarte w adresie (w URL). Tutaj też tak można, a z pomocą przychodzi RouteDataRequestCultureProvider. Ten provider nie jest domyślnie tworzony, więc trzeba stworzyć obiekt tej klasy samemu i dodać go do RequestLocalizationOptions na pierwszym miejscu:

IList<CultureInfo> supportedCultures = new List<CultureInfo>
{
    new CultureInfo("en-US"),
    new CultureInfo("pl"),
};

var localizationOptions = new RequestLocalizationOptions
{
    DefaultRequestCulture = new RequestCulture("en-US"),
    SupportedCultures = supportedCultures,
    SupportedUICultures = supportedCultures        
};

var requestProvider = new RouteDataRequestCultureProvider();
localizationOptions.RequestCultureProviders.Insert(0, requestProvider);

app.UseRequestLocalization(localizationOptions);

Żeby to zadziałało, trzeba jeszcze poinformować router, że w ścieżce są informacje o kulturze:

app.MapControllerRoute(
    name: "default",
    pattern: "{culture=en-US}/{controller=Home}/{action=Index}/{id?}");

Tutaj analogicznie jak przy QueryStringRequestCultureProvider możesz zmienić właściwościami klucze culture i uiculture. Oczywiście musisz pamiętać wtedy o zmianie template’a ścieżki.

Tą metodę wywołaj w metodzie Configure, która jest odpowiedzialna za konfigurację zarejestrowanych serwisów – zrób to przed konfiguracją endpointów.

Pobieranie tłumaczenia na widoku

Teraz już możesz pobierać tłumaczenia. Wystarczy, że dodasz do usingów w widokach: Microsoft.AspNetCore.Mvc.Localization i wstrzykniesz interfejs IStringLocalizer:

@using Microsoft.AspNetCore.Mvc.Localization
@inject IStringLocalizer<LangRes> SharedLocalizer
@inject IStringLocalizer<WebLangRes> WebLocalizer

<h>@WebLocalizer[nameof(WebLangRes.HelloMsg)]</h>

Jak widzisz, możesz wstrzyknąć do jednego widoku kilka takich „lokalizerów”. W zmiennej generycznej określasz tylko klasę z Twoimi zasobami (czyli to, co robiliśmy w tym artykule). Ja tutaj mam dwa takie zasoby – jeden główny w jakimś projekcie współdzielonym (LangRes) i drugi tylko w projekcie MVC (WebLangRes), w którym są teksty bardzo ściśle związane z serwisem www.

Przy takim prostym wywołaniu jak wyżej (tekst w tagu HTML) nic więcej nie trzeba robić. Natomiast jeśli chcesz przekazać tłumaczenie do tag helpera, musisz dołożyć po prostu właściwość Value, np.:

<p>@SharedLocalizer[nameof(LangRes.Contact)]
  <mail prompt="@WebLocalizer[nameof(WebLangRes.MailPrompt)].Value" />
</p>

IHtmlLocalizer

Mamy do dyspozycji jeszcze coś takiego jak IHtmlLocalizer. Działa prawie tak samo jak IStringLocalizer, z tą różnicą, że możesz mu przekazać zasoby z tagami html, np: <b>Hello!</b>. Jednak nie używam go, bo trochę mi śmierdzi wpisywanie kodu html do zasobów.

To tyle. Jeśli czegoś nie zrozumiałeś lub znalazłeś w tekście błąd, daj znać w komentarzu.

Jeśli uważasz ten artykuł za przydatny, udostępnij go.

Podziel się artykułem na: