Wykrywanie języka i tłumaczenie aplikacji, czyli internacjonalizacja oprogramowania

Czym różni się internacjonalizacja od lokalizacji oprogramowania? Jak w praktyce wygląda globalizacja produktu? Czas na wykrywanie języka i tłumaczenie aplikacji SaaS okiem praktyka!

Na początek trochę teorii i wyjaśnienie terminów, które mylą nawet najlepsi 😉

Internacjonalizacja

Internacjonalizacja aplikacji to zaprojektowanie i rozwój projektu w sposób niezależny od języka i kultury regionu, z którego pochodzi użytkownik.

Po pierwsze wszystkie zależne od kultury grafiki i teksty wydzielamy do zasobów projektu. W kodzie odwołujemy się do kluczy, które są podmieniane na odpowiednie wartości w zależności od preferencji lub lokalizacji użytkownika.

Po drugie zapewniamy wsparcie od strony funkcjonalnej, czyli np. obsługujemy różne strefy czasowe, jednostki wag i miar, czy przyjmujemy płatności kartami z różnych krajów w obcych walutach.

Nie wiesz, jak obsługiwać płatności? Zastanawiasz się nad wyborem globalnego dostawcy? Więcej na ten temat znajdziesz w artykule Globalne płatności w aplikacji SaaS, czyli integracja ze Stripem.

Kojarzysz skrót i18n? To właśnie internationalization, czyli i + 18 znaków + n 🙂

Lokalizacja

Lokalizacja z angielskiego localization (l10n) to dostarczenie zasobów np. tekstów, grafik i jednostek dla danego rynku (kultury). Na tym etapie dostosowujemy aplikację do konkretnej grupy odbiorców. Dostarczamy input dla elastycznego oprogramowania wypracowanego w poprzednim kroku.

Jeśli zaimplementowaliśmy obsługę globalnych płatności, to lokalizacją nazwiemy skonfigurowanie odpowiedniej stawki podatku VAT i waluty dla danego kraju.

Głównym elementem lokalizacji jest translacja (t9n), czyli przetłumaczenie aplikacji. Jeśli tłumaczenia (zestaw tekstów dla określonej kultury) trzymamy w oddzielnych plikach, to przygotowanie i umieszczenie w projekcie nowego pliku np. z tekstami w języku niemieckim będzie etapem lokalizacji oprogramowania.

Najczęściej programista zajmuje się internacjonalizacją – zbudowaniem aplikacji w odpowiedni sposób, a właściciel produktu lokalizacją – dostarczeniem wsadu pod konkretny rynek.

Globalizacja

Czym jest globalizacja? Tutaj częściej mówimy o globalizacji produktu lub biznesu niż oprogramowania. Dlaczego? Globalizacja (g11n) składa się z lokalizacji oraz szeregu działań biznesowych mających na celu pozyskanie klienta i sprzedaż na rynkach zewnętrznych.

To, że przetłumaczysz aplikację na język niemiecki, nie oznacza, że zdobędziesz klientów w Niemczech. W zależności od produktu może to wymagać założenia tam spółki, zatrudnienia handlowca, czy supportu w natywnym języku. Dostosowania nie tylko produktu, ale również procesów, środków i narzędzi do innej kultury.

Globalizacja to lokalizacja zinternacjonalizowanej aplikacji + biznes. Brzmi rozsądnie? 😀

Od czego zacząć?

Opcja minimum to internacjonalizacja – przynajmniej tekstów, czyli trzymanie ich w projekcie w formie zasobów. Nawet jeśli na starcie wypuszczasz apkę tylko na rynek polski. Zarządzanie wszystkimi tekstami i komunikatami wyświetlanymi w aplikacji jest dużo łatwiejsze, gdy znajdują się w jednym pliku.

Jeśli piszesz software na zamówienie, to możesz podesłać taki plik swojemu klientowi do weryfikacji, przetłumaczenia, czy sprawdzenia pod kątem błędów językowych. W przyszłości dostarczenie wsparcia dla kolejnej kultury (lokalizacja) sprowadzi się do umieszczenia w projekcie dodatkowego pliku. Hardkodowanie tekstu w kodzie jest po prostu słabe.

Co z globalizacją? Najlepiej rozpocząć sprzedaż i dystrybucję aplikacji na rynku lokalnym – jeśli jest wystarczająco duży. Opracować procesy i dobrze poznać swojego klienta, zanim ruszysz na podbój kolejnych regionów.

Zawsze warto mieć uniwersalną wersję anglojęzyczną i obsługiwać płatności w EUR lub USD – nawet gdy na początku celujesz tylko w Polskę. Jeśli obcokrajowcy trafią na Twoje rozwiązanie, mogą stać się pierwszymi zagranicznymi klientami i zapoczątkować globalizację 🙂

Frontend (Angular)

Jak technicznie ograć tłumaczenia w aplikacji? Zacznijmy od frontendu, który najczęściej piszę w Angularze. Na początek wykrywam język przeglądarki.

@Injectable()
export class UserService {

    getCulture(): string {
        const nav: any = window.navigator;
        if (nav.languages) {
            return nav.languages[0];
        } else {
            return nav.userLanguage || nav.language;
        }
    }

    getLanguage(): string {
        return this.getCulture().split('-')[0];
    }
}

Następnie instaluję bibliotekę @ngx-translate/core wraz z @ngx-translate/http-loader i konfiguruję zgodnie z dokumentacją. Jednym z jej elementów jest ustawienie aktualnego języka na starcie aplikacji – w moim przypadku w app.component.ts.

// this language will be used as a fallback when a translation isn't found in the current lang
translate.setDefaultLang('en');

// the lang to use, if the lang isn't available, it will use the current loader to get them
translate.use(this.userService.getLanguage());

Teraz tłumaczenia dla konkretnych języków mogę umieścić w oddzielnych plikach JSON – u mnie assets/i18n/en.json oraz assets/i18n/pl.json. Oczywiście mógłbym również bazować na kulturze i np. stworzyć osobne pliki dla Wielkiej Brytanii (en-GB) i USA (en-US).

Jak wygląda struktura pliku z tłumaczeniami? Poniżej przykład dla języka polskiego.

{
  "FirstView": {
    "Title": "Tytuł pierwszego widoku"
  },
  "SecondView": {
    "Title": "Tytuł drugiego widoku",
    "Button": "Etykieta przycisku"
  }
}

Na widokach w HTML odwołujemy się do pojedynczych tekstów po kluczach z pliku JSON i po robocie.

<h1>{{'SecondView.Title' | translate}}</h1>
<button>{{'SecondView.Button' | translate}}</button>

No dobra ale co z komunikatami zwracanymi przez backend? Skąd wiem w jakim języku wysłać użytkownikowi maila lub zwrócić komunikat błędu?

Tworzę interceptor, który do każdego zapytania wychodzącego z frontu dokleja w nagłówku informację o kulturze przeglądarki.

@Injectable()
export class CultureInterceptor implements HttpInterceptor {
    private userService: UserService;

    constructor(private injector: Injector) {
    }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
        if (!this.userService) {
            this.userService = this.injector.get(UserService);
        }

        return next.handle(
            req.clone({
                headers: req.headers.set('Culture', this.userService.getCulture())
            })
        );
    }
}

Uwaga! Teoretycznie mógłbym wykorzystać domyślny nagłówek Accept-Language, który zazwyczaj dokleja przeglądarka – zamiast tworzyć i ustawiać własny.

Problem? Niektóre przeglądarki nie dodają go dla zapytań cross-origin lub POST. Dlatego wolę własny, który w pełni kontroluję i mogę wysyłać analogicznie np. z poziomu aplikacji mobilnej.

Należy teraz na podstawie tego nagłówka ustawić właściwą kulturę po stronie serwera.

Backend (ASP.NET Core)

Piszę API w ASP.NET Core, więc konfigurację lokalizacji umieszczam w klasie Startup. Ustawiam wspierane kultury, czyli te zlokalizowane, dla których przygotowałem pliki z tłumaczeniami.

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<RequestLocalizationOptions>(options =>
    {
        var defaultCulture = "en";
        var supportedCultures = new[]
        {
            new CultureInfo(defaultCulture),
            new CultureInfo("pl")
        };

        options.DefaultRequestCulture = new RequestCulture(defaultCulture);
        options.SupportedCultures = supportedCultures;
        options.SupportedUICultures = supportedCultures;

        options.RequestCultureProviders = new[]
        {
            new CustomRequestCultureProvider(async context =>
            {
                string culture = context.Request.Headers["Culture"];
                if (!string.IsNullOrEmpty(culture))
                {
                    return new ProviderCultureResult(culture);
                }
                return new ProviderCultureResult(defaultCulture);
            })
        };
    });
    ...
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    app.UseRequestLocalization();
    ...
}

Nadpisuję listę RequestCultureProviders pojedynczym providerem, który odczytuje wartość nagłówka Culture. Jeśli użytkownik ma możliwość zmiany języka z poziomu aplikacji, to w tym miejscu mógłbym również pobierać wybrany język z bazy danych.

Domyślnie RequestCultureProviders zawiera 3 providery, które próbują pobrać język kolejno z query stringa, cookie lub właśnie nagłówka Accept-Language. Nie korzystam z nich, ponieważ tutaj również chcę mieć pełną kontrolę i świadomość.

Wszystkie komunikaty umieszczam w plikach z rozszerzeniem .resx – znanych i lubianych w środowisku .NET 🙂

Plik zasobów .resx

Osobno trzymam komunikaty walidacyjne (Validation.en.resx, Validation.pl.resx) oraz szablony maili lub wiadomości tekstowych (Template.en.resx, Template.pl.resx).

var model = new EmailInput()
{
    To = email,
    Subject = Resource.Template.ResetPasswordSubject,
    HtmlBody = Resource.Template.ResetPasswordBody
};

W kodzie używam wyłącznie kluczy, jak na przykładzie powyżej. That’s it!

Aplikacje mobilne

Aplikacje mobilne najczęściej korzystają z wbudowanych mechanizmów lokalizacji. Google opisał cały proces TUTAJ, a przewodnik od Apple znajdziesz TUTAJ.

W świecie tysięcy modeli urządzeń mobilnych odpowiednie zarządzanie zasobami aplikacji to podstawa. Jeśli tego nie potrafisz lub nie masz programisty, który zrobi to za Ciebie, to zapomnij o świetnym UX.

Landing / blog (WordPress)

Co z blogiem i stronami produktowymi? Większość moich stron docelowych stoi na WordPressie. Pisałem o nim więcej w artykule 10 wtyczek do WordPressa, które musisz znać.

Wypróbowałem wiele różnych wtyczek do translacji WordPressa i tylko Polylang spełniła moje oczekiwania.

Zakładka Strings translations wyświetla wszystkie łańcuchy znaków zaszyte w szablonie – możesz tłumaczyć je one by one.

Co więcej, do każdej strony możesz podpiąć jej przetłumaczone wersje w innych językach – po prawej stronie edytora pojawia się niewielki widget.

Wiązanie przetłumaczonych stron

Wtyczka udostępnia również kontrolkę do zmiany języka tzw. language switcher, który możesz umieścić np. w menu swojej witryny.

Language switcher

Chcesz zobaczyć ją w akcji? Zapraszam do mnie na Customer24 😉

Podsumowanie

Wydzielanie wszystkich tekstów do zasobów może wydawać się zbędne i czasochłonne. Jeśli jednak nie uwzględnisz internacjonalizacji na starcie projektu, to późniejsze tłumaczenie aplikacji i ewentualna globalizacja będzie o wiele bardziej kosztowna i czasochłonna.

Według mnie warto tworzyć elastyczne rozwiązania, hardkodować jak najmniej i zawsze mieć uniwersalną wersję anglojęzyczną.

Co Ty o tym sądzisz? Jak podchodzisz do tłumaczeń w swoim projekcie? Z jakich bibliotek i technologii korzystasz? Daj znać w komentarzu! 😊

Podobał Ci się ten artykuł?

Jeśli tak, to udostępnij go w Social Media i zostaw maila o TUTAJ, aby otrzymywać powiadomienia o nowych artykułach i materiałach!