Wstęp

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.:

NO_FILE = "Nie znaleziono pliku"
SAVE_FILE_PROMPT = "Czy chcesz zapisać plik?"

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 ActionEmbedded Resource – oznacza, że ten plik ma być wbudowanym zasobem – stanie się częścią Twojej dllki lub execa.
Copy to output directoryDo 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:
Domyślny edytor zasobów – widok LangRes.resx

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:


Jeśli czegoś nie rozumiesz albo znalazłeś błąd w artykule, daj znać w komentarzu 🙂

Podziel się artykułem na: