Tłumaczenie aplikacji internetowych – RAZOR
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:
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.As
pNetCore.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.