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 WinForms i WPF. Poprzedni artykuł – Tłumaczenie aplikacji cz. 3 – jak to ogarnąć? – daje całą podstawę.
Tłumaczenia w WinForms
W WinForms nie ma żadnego zmyślnego sposobu na ładowanie tłumaczeń. Po prostu każdemu przyciskowi, labelowi itd musisz zmienić TEXT w runtime. Chyba, że wymyślisz swój własny sposób, który zadziała automatycznie. Ale chyba więcej z tym nerwów niż pożytku.
Generalnie robisz to dokładnie tak samo, jak robiłbyś to w konsoli – dokładnie tak jak opisane w artykule podstawowym.
Pamiętaj, żeby domyślnie nadawać teksty w języku angielskim – wtedy jeśli czegoś nie przetłumaczysz, użytkownicy zobaczą teksty w tym właśnie języku.
Tłumaczenia w WPF
W WPF skorzystamy z ustrojstwa, co się zowie MarkupExtension. Jeśli nie wiesz co to, to odsyłam do netu: „WPF markup extension”, być może kiedyś opiszę ten mechanizm.
W skrócie – to coś, dzięki czemu możesz tworzyć własne tagi XAML. Coś jak {Binding...}
Teraz popatrz na ten fragment kodu:
<GroupBox Header="{app:Localize Receipt}" />
Tutaj widzisz mój markup extension – Localize. Efektem tego kodu będzie pobranie zasobu o kluczu Receipt i wartość tego zasobu będzie widoczna w nagłówku GroupBoxa – w Polsce: „Paragon”, wszędzie indziej: „Receipt”. Super? Ja się jaram 🙂
A teraz zobaczmy, jak coś takiego osiągnąć. Generalnie łatwo, prosto i przyjemnie…
MarkupExtension do tłumaczeń
Stwórz taką klasę najlepiej gdzieś w projekcie WPF:
[ContentProperty("ResId")]
class LocalizeExtension : MarkupExtension
{
public string ResId { get; set; }
public LocalizeExtension()
{
}
public LocalizeExtension(string ResId)
{
this.ResId = ResId;
}
public override object ProvideValue(IServiceProvider serviceProvider)
{
if (string.IsNullOrWhiteSpace(ResId))
return "<???>";
string result = LangRes.ResourceManager.GetString(ResId);
if (string.IsNullOrEmpty(result))
return $"<? {ResId} ?>";
else
return result;
}
}
Wystarczy napisać klasę, która dziedziczy po MarkupExtension.
W linijce 1 podajesz domyślną właściwość… Tzn. gdybym tego nie zrobił, musiałbym kod w XAML napisać tak:
<GroupBox Header="{app:Localize ResId=Receipt}"/> //zwróć uwagę na obecność ResId
Klasa ma jedną metodę – ProvideValue i to w niej dzieje się cała magia. Po prostu pobieram stringa z zasobów na podstawie przekazanego klucza.
Ja tu sobie zrobiłem taki myk, że w razie jakbym nie dodał jakiegoś tłumaczenia, wtedy zamiast konkretnego stringa (którego nie ma w zasobach) zobaczę nazwę tego klucza w nawiasach ostrych. Dzięki temu wiem, że danego tłumaczenia nie ma w zasobach. Pozwalam sobie na taką nonszalancję, bo sprawdzam każde okienko, które robię.
I to właściwie tyle. Po utworzeniu takiej klasy, możesz skompilować projekt i używać swojego markup extension.
Być może nie wiesz skąd się bierze to app w kodzie:
<GroupBox Header="{app:Localize Receipt}"/>
To po prostu alias na namespace, w którym masz swoją klasę LocalizeExtension. Musisz go oczywiście zadeklarować na początku pliku XAML analogicznie do innych, np:
gdzie MojaAplikacja to namespace, w którym znajduje się Twoja klasa. Oczywiście to nie musi być app. To może być cokolwiek, ale mam nadzieję, że to wiesz.
To wszystko jeśli chodzi o tłumaczenie aplikacji desktopowych. Jeśli masz jakieś inne pomysły, podziel się w komentarzu.
Mam nadzieję, że przeczytałeś dwa poprzednie artykuły z serii o globalizacji i przygotowaniu do lokalizacji. Jeśli nie lub jeśli nie zastosowałeś się do nich, to uciekaj szybciutko z tego artykułu i zrób to wszystko, o co prosiłem. Do poczytania część pierwsza, która mówi czym w ogóle jest internacjonalizacja i jak do tego podejść; no i część druga, która przygotowuje do TEJ części artykułu.
Lokalizacja
Proces tłumaczenia aplikacji nazywa się lokalizacją. W dużym skrócie polega na:
tłumaczeniu
wybraniu sposobu przechowywania i ładowania tłumaczeń – na tym punkcie skupimy się dzisiaj
Pamiętaj, że słowo „OK” może brzmieć po węgiersku jak „Haszaragódgad”. Więc jeśli na sztywno ustawiasz szerokości przycisków, będziesz mieć problem 🙂
Jak ogarnąć tłumaczenie?
Jeśli chodzi o tłumaczenie aplikacji, mamy kilka możliwości:
oddać stringi do tłumacza, który zwróci je przetłumaczone na konkretny język
użyć automatycznego translatora (np. Google Translate). Nie jest to idealne rozwiązanie, bo jest duża szansa na to, że aplikacja zostanie przetłumaczona w dziwny sposób, np: „Czy chcesz uratować plik? Wciśnij ok, aby plik został uratowany„. Z moich testów wynika, że Google Translator nie do końca radzi sobie z tłumaczeniem z języka polskiego, natomiast całkiem nieźle mu idą tłumaczenia z języka angielskiego. Więc jeśli masz całe frazy, możesz wrzucić je do googla i tłumaczyć z angielskiego na docelowy język (np. na niemiecki).
Do głowy przychodzi mi jeszcze jedna sztuczka – tłumaczenie „ad hoc” – tzn. na gorąco wysyłamy stringi do google translatora. Oczywiście ma to tylko minusy (łącznie z opłatami), więc nie robimy tego.
Generalnie odradzam używanie automatycznych translatorów. Jeśli widzę tak przetłumaczoną aplikację, od razu ustawiam język na angielski, bo w tak tłumaczonym polskim nie idzie się połapać. Tak samo jest, gdy czytam dokumentacje Microsoftu – po polsku po prostu nie idzie nic zrozumieć. Polski jest jednak jednym z najtrudniejszych języków świata, więc może tu leży pies pogrzebany…
Moja rada – tłumacz tylko na język, który znasz lub możesz się z kimś skonsultować.
Niezależnie od tego w jaki sposób otrzymasz tłumaczenia, powinieneś dać aplikację do przetestowania komuś, kto zna dany język. W tym momencie może wyjść kilka kwiatków językowych związanych chociażby z odmianą przez przypadki.
Przechowywanie tłumaczeń
Teraz zastanówmy się w jaki sposób przechowywać teksty. Rzecz jasna, nie możesz ich mieć bezpośrednio w kodzie. One muszą być skądś pobierane. Mamy tutaj znowu kilka możliwości:
teksty są w bazie danych (spotykałem się z takimi rozwiązaniami głównie w przeszłości i głównie w aplikacjach internetowych); jeśli mamy aplikację webową, a treści są dynamiczne i wielojęzyczne, to właściwie jest to jedyna opcja. Ale w artykule skupiam się bardziej na tekstach statycznych.
teksty są w pliku lokalizacyjnym
teksty są w zasobach
Pliki lokalizacyjne
Mówimy tutaj o plikach tekstowych. Mogą być otwierane i zmieniane przez dowolną osobę w dowolnym edytorze. Zazwyczaj składają się z klucza i wartości, np.:
Dobrym rozwiązaniem tutaj jest posiadanie osobnych plików dla każdego języka. Pchanie wszystkich języków do jednego pliku robi po prostu burdel i utrudnia pracę tłumaczom, a także Tobie (potem trzeba to mergować). Takie pliki mogą posiadać dodatkowo różne sekcje, przez co można je traktować jako pliki INI.
Plusy takiego rozwiązania:
każdy może dołożyć nowy język
każdy może przetłumaczyć aplikację
szybko możesz mieć dużą ilość języków w aplikacji (zwłaszcza jeśli masz znajomych w wielu krajach)
Minusy:
każdy może dołożyć nowy język
każdy może przetłumaczyć aplikację
szybko możesz mieć dużą ilość języków w aplikacji (zwłaszcza jeśli masz znajomych w wielu krajach).
Czy to błąd? Nie, specjalnie zrobiłem te cechy zarówno plusami, jak i minusami. Np. Twoja aplikacja może nie być przygotowana na niektóre języki (np. te pisane od prawej do lewej). A ktoś na taki język przetłumaczy. Aplikacja będzie się dziwnie zachowywać i wyglądać. Poza tym duża ilość tłumaczeń w krótkim czasie może być problematyczna, jeśli programujesz sam lub masz mały zespół. Te tłumaczenia trzeba utrzymywać. Wraz z rozwojem aplikacji, pewne stringi znikają i pojawiają się kolejne.
Poza tym nigdy nie wiesz, kto robi tłumaczenia. Jeśli masz fajną społeczność, która się przykłada, to ok. Ale jeśli ludzie będą tłumaczyć automatami…
Część z tych problemów odpada, jeśli aplikacja jest webowa. Wtedy to częste zastosowanie. Jednak format pliku niekoniecznie musi być taki prosty. Możesz trafić na inne, np.: Android XML, Angular Translate, i18next.
Jak tworzyć pliki lokalizacyjne?
Ze względu na to, że to zwykłe pliki tekstowe, można tworzyć je ręcznie w dowolnym edytorze. Jednak często używa się innych narzędzi, skryptów, czy też usług. Automatyczne tworzenie takich plików zwalnia programistę z głupiej, błędogennej pracy i właściwie zapewnia, że pliki będą utworzone poprawnie.
Zasoby – czyli lokalizacja w .NET
Microsoft od dawna woli lokalizację za pomocą zasobów. Ja też raczej idę tą ścieżką, chociaż w przeszłości zdarzało się inaczej. W związku z tym, że jest to „prawilna” droga w .NET, na tym właśnie sposobie się skupimy.
Tworzenie zasobu językowego – przeczytaj to
Teksty trzymasz w zasobach. Wyobraź sobie zasób jako swego rodzaju plik, który jest wkompilowany do Twojego programu.
Zasób dodajesz do konkretnego projektu. Zatem w każdym projekcie możesz mieć osobne zasoby lub możesz mieć jeden projekt z samymi zasobami. Które rozwiązanie lepsze? Jak zwykle – to zależy 🙂 Aby dodać taki plik:
kliknij prawym klawiszem myszy na projekt, w którym chcesz dodać zasoby
wybierz opcję: Add -> New Item
w okienku wyszukiwania zacznij wpisywać „resource”. Na liście zobaczysz element „Resource File”
Teraz, żeby ten plik uczynić plikiem językowym, musisz dodać mu kod języka do nazwy, np: Resource.en-US.resx Resource.pl-PL.resx Resource.en.resx
To po prostu określenie języka (przed myślnikiem) i regionu (po myślniku).
Mamy tutaj 3 pliki
en-US – zasoby dla języka angielskiego amerykańskiego (US); angielski brytyjski byłby us-GB
pl-PL – zasoby dla języka polskiego; jako, że język polski jest używany tylko w jednym regionie, nie ma większego znaczenia, czy wpiszesz pl, czy pl-PL… Teoretycznie…
en – zasoby dla języka angielskiego (bez określenia regionu).
UWAGA! Z zasady powinieneś tworzyć pliki językowe w kolejności – od ogółu do szczegółu. Czyli np. jeśli masz plik Resource.en-US.resx to powinieneś też mieć bardziej ogólny: Resource.en.resx a najlepiej najbardziej ogólny: Resource.resx.
UWAGA! Teraz będzie ważne. Kliknij na utworzony plik prawym klawiszem myszy i z menu wybierz Properties. Pojawi Ci się okienko z właściwościami tego elementu. Zwróć na nie uwagę:
Build Action – Embedded Resource – oznacza, że ten plik ma być wbudowanym zasobem – stanie się częścią Twojej dllki lub execa. Copy to output directory – Do not copy – oznacza, że plik nie zostanie skopiowany do katalogu wyjściowego podczas budowania (no bo po co, skoro jest częścią execa) Custom Tool – to będzie narzędzie użyte do procesowania tego pliku podczas budowania. I tutaj ważnym jest, żebyś upewnił się, że każdy z tych plików ma tutaj wpisane ResXFileCodeGenerator. Dzięki temu narzędzie zadziała, a VS wygeneruje potrzebne później pliki z kodami.
Cały mechanizm lokalizacji działa w taki sposób, że framework rozpozna, jaki język jest używany przez użytkownika i w jakim jest on regionie. Jeśli taką aplikację uruchomi Anglik, framework automagicznie wybierze plik Resource.en.resx. Dlatego, że nie ma pliku en-GB. Weźmie pod uwagę tylko ten pierwszy kod. Jeśli program zostanie uruchomiony przez Amerykanina, zostanie użyty en-US (język angielski dla regionu US)
A jeśli nie daj Boże Francuz uruchomi naszą aplikację? Nie mamy przecież zasobu fr. Dlatego powinniśmy mieć jeszcze jeden zasób o nazwie Resource.resx. Bez określenia kodu języka. To będzie domyślny plik dla frameworka. Tak naprawdę, jeśli tworzę aplikację dwujęzyczną (np. polski i angielski), mam tylko dwa pliki zasobów:
resource.resx – domyślny, wszystko po angielsku
resource.pl.resx – tłumaczenia polskie
Dzięki takiemu rozwiązaniu, Francuz od razu zobaczy angielskie tłumaczenia. Tak samo jak Włoch, Serb, czy Szwed. A nawet Anglik, czy Amerykanin. Tylko Polak zobaczy polskie.
A od czego to zależy? Jeśli chodzi o aplikacje desktopowe, to od ustawień systemowych. Przecież w systemie masz wklepany zarówno swój język, jak i region. Jednak można to zmienić w kodzie, zmieniając właściwość CurrentUICulture w odpowiednim wątku aplikacji.
Dlaczego lepiej mieć pl niż pl-PL
A teraz muszę wyjaśnić Ci mały myk, który kiedyś zajął mi godzinę bezsensownej pracy (dlatego też wyżej pisałem – od ogółu do szczegółu). Tworzyłem aplikację webową i okazało się, że na jednej przeglądarce (Chrome) wszystko działało, natomiast na drugiej (Firefox) nie było polskich tłumaczeń. Problemem było to, że plik miałem nazwany: resource.pl-PL.resx.
Przeglądarka Chrome zwracała mi kod języka pl-PL, natomiast Firefox zwracał tylko pl – bez określenia regionu. Nie było pliku resource.pl.resx, dlatego wzięty został pod uwagę tylko plik domyślny – z angielskimi tłumaczeniami. Miej to na uwadze.
Zmiana języka w przeglądarce
A skąd przeglądarka wie, jaki ma brać język? Z własnych ustawień:
Firefox – Ustawienia -> Ogólne -> Język i alternatywne:
Analogicznie jest w Chrome: Ustawienia -> Zaawansowane -> Języki
Generalnie przeglądarka może mieć ustawione kilka języków, które obsługuje z określoną wagą. O tym później.
Lokalizacja w praktyce
Koniec teorii, bierzemy się do roboty. Pokażę Ci, jak używać mechanizmu lokalizacji w różnych technologiach. I jak używać go wygodnie.
Wspólnym mianownikiem wszystkich technologii jest utworzenie plików lokalizacyjnych, tak jak pokazałem to wyżej. I tak, jak mówiłem – możesz mieć osobne pliki w osobnych projektach. Możesz mieć też jeden projekt, w którym znajdują się Twoje wszystkie zasoby językowe.
A więc:
stwórz 2 pliki w jakimś projekcie: LangRes.resx i LangRes.pl.resx – w taki sposób, jak opisany wyżej
kliknij dwukrotnie na jeden z tych plików – otworzy się edytor zasobów. Dodaj po jednym elemencie. W pierwszej kolumnie podajesz klucz, pod jakim będzie dana wartość. Np: „HelloMsg”. W drugiej kolumnie podajesz konkretną wartość dla danego języka. Tak jak na obrazku poniżej:
Oczywiście są różne programy do edytowania plików resx. Niektóre pokazują kilka języków jednocześnie, co może ułatwiać tłumaczenie. Ja dawno z niczego takiego nie korzystałem, bo domyślny edytor w zupełności mi wystarcza.
Kolumna Comment, to kolumna w której możesz wpisać sobie jakiś komentarz pomagający przetłumaczyć na inny język. Np. że ma to być dopełniacz albo co tam uważasz za słuszne. Możesz to pole zostawić puste.
Dodaj to samo tłumaczenie w drugim pliku – LangRes.pl.resx
UWAGA!
Zwróć uwagę na pole Access Modifier. Ono może przyjąć 3 wartości:
internal
public
No code generation
No code generation oznacza, że podczas budowania aplikacji dla tego pliku nie zostanie wygenerowany żaden kod. Zdecydowanie NIE CHCEMY TEGO. Chcemy, żeby Visual Studio wygenerował odpowiedni kod. Dlatego upewnij się, że masz tam specyfikator dostępu ustawiony na internal lub public.
Klasa wygenerowana przez Visual Studio będzie miała ten specyfikator dostępu. A więc, jeśli wybierzesz INTERNAL, nie będziesz mógł się odwoływać do tych zasobów z innych swoich projektów. Jeśli wybierzesz PUBLIC, klasa będzie publiczna i odwołasz się bez problemów.
Jeśli więc tworzysz zasoby językowe w osobnym projekcie (współdzielonym), zawsze wybieraj tam PUBLIC.
Super, zbuduj teraz tę aplikację.
Pobieranie zasobów
Sprawa jest banalna, spójrz na ten kod w aplikacji konsolowej:
using System.Globalization;
//...
static async Task Main(string[] args)
{
Console.WriteLine("Tekst zgodny z aktualną kulturą: " + LangRes.HelloMsg);
LangRes.Culture = new CultureInfo("en-US");
Console.WriteLine("Tekst zgodny z kulturą angielską: " + LangRes.HelloMsg);
LangRes.Culture = new CultureInfo("fr");
Console.WriteLine("Tekst zgodny z kulturą francuską: " + LangRes.HelloMsg);
Console.ReadKey();
}
Jak widzisz, powstała specjalna klasa statyczna (podczas kompilacji) LangRes -> nazwa tej klasy to po prostu nazwa główna Twojego zasobu. W tej klasie będą wszystkie klucze obecne w Twoim zasobie. W taki właśnie sposób możesz używać tłumaczeń w swoim kodzie w .Net Framework.
Pobieranie zasobów przez serwis i tłumaczenia w innym projekcie.
Zauważ, że klasa LangRes jest oznaczona jako internal. To oznacza, że wykorzystać ją możesz tylko z tego projektu, w którym się ona znajduje. Ale być może chciałbyś ją umieścić w innym projekcie i mieć ją nadal oznaczoną jako internal. A może po prostu chcesz mieć serwis, który dostarczy tłumaczenia?
stwórz więc nowy projekt Class Library (.Net Framework)
dodaj do niego te dwa zasoby (poprzednie możesz usunąć)
stwórz publiczną klasę o nazwie Localizer (możesz sobie zrobić z tego interfejs)
Spójrz, jak wygląda moja klasa Localizer (wraz s interfejsem):
public interface ILocalizer
{
string this[string key] { get; }
}
public class Localizer : ILocalizer
{
public string this[string key]
{
get
{
return LangRes.ResourceManager.GetString(key);
}
}
}
Ta klasa jest równie prosta, co przydatna. Zwłaszcza, gdy używasz różnych bibliotek, które po swojemu ogarniają tłumaczenia. Możesz wtedy wszystko zrobić na tym poziomie. Pomijam w tym momencie dependency injection, ale zobacz, jak użyję tego w poprzednim programie:
ILocalizer localizer = new Localizer();
Console.WriteLine("Tekst zgodny z aktualną kulturą: " + localizer[nameof(LangRes.HelloMsg)]);
Tutaj jednak już nie wystarczy zmiana kultury w LangRes. Aby pobrać tekst dla innego języka, musiałbyś posłużyć się odpowiednim przeciążeniem metody GetString, którą używasz w Localizerze, np:
public interface ILocalizer
{
string this[string key] { get; }
string Localize(string key, CultureInfo culture);
}
public class Localizer : ILocalizer
{
public string this[string key]
{
get
{
return LangRes.ResourceManager.GetString(key);
}
}
public string Localize(string key, CultureInfo culture)
{
return LangRes.ResourceManager.GetString(key, culture);
}
}
I wtedy poprzedni program może wyglądać tak:
static async Task Main(string[] args)
{
ILocalizer localizer = new Localizer();
Console.WriteLine("Tekst zgodny z aktualną kulturą: " + localizer[nameof(LangRes.HelloMsg)]);
Console.WriteLine("Tekst zgodny z kulturą angielską: " + localizer.Localize(nameof(LangRes.HelloMsg), new CultureInfo("en-US")));
Console.WriteLine("Tekst zgodny z kulturą francuską: " + localizer.Localize(nameof(LangRes.HelloMsg), new CultureInfo("fr")));
Console.ReadKey();
}
Dobra rada – NIGDY w kluczu nie używaj stringa. Zawsze posłuż się operatorem nameof tak jak ja wyżej. To daje Ci Intellisensa – dokładnie wiesz, jakie masz klucze w pliku zasobów i nie musisz ich pamiętać. Poza tym, jeśli usuniesz kiedyś jakiś klucz lub zmienisz mu nazwę, to program się nie skompiluje i da Ci szansę uaktualnić kod.
Pobieranie tekstów w różnych technologiach
Postanowiłem ten fragment podzielić na odrębne artykuły. Poniżej masz listę, która będzie aktualizowana:
Obsługujemy pliki cookies. Jeśli uważasz, że to jest ok, po prostu kliknij "Akceptuj wszystko". Możesz też wybrać, jakie chcesz ciasteczka, klikając "Ustawienia".
Przeczytaj naszą politykę cookie