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 🙂
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.
Wtyczka udostępnia również kontrolkę do zmiany języka tzw. language switcher, który możesz umieścić np. w menu swojej witryny.
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! 😊