Czasami tak bywa, że sukces produktu niesie nowe wyzwania dla programistów. Jednym z takich wyzwań może być przetłumaczenie tekstów wyświetlanych w menu na inne języki i obsługa tego w prosty i niezawodny sposób. W dzisiejszym wpisie pokażę jak ustrukturyzować kod zawierający menu i jak łatwo zaimplementować tłumaczenia wykorzystując tablice.

Odpowiednia struktura projektu

Wprowadzenie takiej zmiany jak nowy język menu jest dosyć proste i schematyczne, jeżeli nasz projekt ma odpowiednią strukturę. Niestety bardzo często nie ma. A jakie są najczęstsze problemy z kodem obsługującym wyświetlacz?

Moim zdaniem to brak odpowiednich warstw abstrakcji i podziału zadań. Często zdarza się, że w jednym miejscu mieszamy obsługę logiki naszego systemu, ekranu LCD, przycisków i logiki menu. Tekst do wyświetlenia jest przekazywany bezpośrednio w wywołaniach funkcji wyświetlacza LCD. Te funkcje są rozproszone po całym kodzie. W ten sposób może i nawet uda nam się stworzyć działający kod, ale na dłuższą metę trudno do niego cokolwiek dodać, albo zmienić.

Dodanie nowego języka menu wymaga wtedy zmian w ogromnej części projektu. Poza tym nie możemy zbyt łatwo zmienić elementów menu, bo w dziwny sposób zależą od logiki. Nie możemy łatwo dodać obsługi podobnego menu z telefonu, czy ze strony internetowej.

Chyba każdy, kto pierwszy raz podchodził do tematu menu na wyświetlaczu miał podobne problemy.

Model – View – Controller

Nasze problemy z menu na ekranie LCD można uogólnić. Programiści od lat szukają sposobów na efektywną pracę z interfejsami użytkownika. Każdy taki interfejs prezentuje dane odczytane z systemu, posiada jakieś mechanizmy do interakcji z systemem i zawiera wewnętrzną logikę odpowiedzialną za działanie całego systemu. Dokładnie to samo robi nasz interfejs składający się z wyświetlacza i przycisków. Możemy więc użyć popularnego wzorca projektowego do interfejsów użytkownika – Model – View – Controller.

Obrazek z wikipedii: https://pl.wikipedia.org/wiki/Model-View-Controller

Oddzielamy więc od siebie logikę systemu, prezentację danych oraz obsługę wejścia od użytkownika. Logika systemu nie wie nic o widokach i kontrolerach. Wystawia jedynie interfejsy do odczytywania danych i podawania wejść.

Widok zajmuje się prezentowaniem danych odczytanych z tych interfejsów. Możemy mieć wiele różnych widoków. Poza menu na przykład wspomniana strona internetowa, aplikacja debugowa, port szeregowy, czy nawet diody LED. Tak samo może być wiele różnych źródeł sygnałów wejściowych, nie tylko przyciski. Czasem rozdzielenie widoku od kontrolera może nie być do końca możliwe. Czasem będziemy potrzebowali jeszcze jeden element, który je spina. Ale na pewno możemy odseparować interfejs od logiki systemu. Reszta zależy już od konkretnej aplikacji.

Tablice z tłumaczeniami

Mając odpowiednią separację menu od logiki systemu możemy przystąpić do właściwej obsługi tłumaczeń. W tym celu możemy użyć tablic i enumów. Na początek zobaczmy sobie prosty przykład z tłumaczeniem nazw dni tygodnia.

Możesz samemu poeksperymentować z przykładem w Godbolcie:
https://godbolt.org/z/jc7aMaYc4

Nasze dni tygodnia to trzyliterowe stringi. Tablica dla języka angielskiego wygląda tak:

const char * const weekdays_en[] =
{
    "MON",
    "TUE",
    "WED",
    "THU",
    "FRI",
    "SAT",
    "SUN",
};

Z kolei dla polskiego tak:

const char * const weekdays_pl[] =
{
    "PON",
    "WT ",
    "SR ",
    "CZW",
    "PT ",
    "SOB",
    "NIE",
};

Używam podwójnego consta, ponieważ treść stringów jest stała, a nie chcę również podmieniać elementów w tablicy.

Następnie tworzę sobie kolejną tablicę zawierającą dwie poprzednie:

const char * const * const weekdays[LANG_SIZE] =
{
    weekdays_en,
    weekdays_pl,
};

Do wyboru konkretnego tłumaczenia będę się posługiwał enumem:

enum
{
    ENG,
    POL,
    LANG_SIZE,
};

Przyda się również drugi enum do wyboru odpowiedniego dnia tygodnia:

enum
{
    MONDAY,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY,
    WEEKDAYS_SIZE,
};

I teraz jeżeli chcę wyświetlić środę po polsku – piszę:

printf(weekdays[POL][WEDNESDAY]);

A kiedy po angielsku:

printf(weekdays[ENG][WEDNESDAY]);

Oczywiście zamiast printfa mogę użyć dowolnej innej funkcji do obsługi wyświetlacza.

Dodawanie kolejnego języka

Jak już się pewnie domyślasz – dodanie kolejnego języka jest proste i dosyć schematyczne. Mamy gotowy szablon i możemy go dowolnie powielać. Tworzymy więc tablicę tłumaczeń dla języka niemieckiego:

const char * const weekdays_ger[] =
{
    "Mo.",
    "Di.",
    "Mi.",
    "Do.",
    "Fr.",
    "Sa.",
    "So.",
};

Dodajemy ją do listy wszystkich dostępnych tłumaczeń:

const char * const * const weekdays[LANG_SIZE] =
{
    weekdays_en,
    weekdays_pl,
    weekdays_ger,
};

Dodajemy kolejny label do enuma z obsługiwanymi językami:

enum
{
    ENG,
    POL,
    GER,
    LANG_SIZE,
};

I teraz jeżeli chcemy wyświetlić tekst po niemiecku – piszemy:

printf(weekdays[GER][WEDNESDAY]);

Proste, prawda?

Zobaczmy teraz inny przykład.

Centrala klimatyzacyjna

Załóżmy, że chcemy zrobić menu do obsługi centrali klimatyzacyjnej. Z naszego menu będzie można sprawdzić temperatury, bieg wentylatora, czy moc nagrzewnicy.

Kod przykładu w Godbolcie: https://godbolt.org/z/8KeTKbPPx

Po raz kolejny przygotowujemy tablicę z tekstami w języku polskim:

const char * const labels_pl[] =
{
    "Temperatura pomieszczenia",
    "Temperatura nawiewu",
    "Temperatura zewnetrzna",
    "Temperatura zadana",
    "Bieg wentylatora",
    "Moc nagrzewnicy [%]",
    "Tryb nocny",
    "Tryb serwisowy",
};

Oraz po angielsku:

const char * const labels_en[] = 
{
    "Room Temp",
    "Air Temp",
    "Out Temp",
    "Set Temp",
    "Fan speed",
    "Heater power [%]",
    "Night mode",
    "Service mode",
};

Niemiecki sobie tym razem odpuszczę 😀

Mamy tablicę zawierającą wszystkie nasze tłumaczenia:

const char * const * const labels[LANG_SIZE] =
{
    labels_en,
    labels_pl,
};

Enum z listą dostępnych języków:

enum lang
{
    ENG,
    POL,
    LANG_SIZE,
};

Oraz enum opisujący indeksy poszczególnych tekstów w tablicy tłumaczeń:

enum label_id
{
    ROOM_TEMP,
    AIR_TEMP,
    OUT_TEMP,
    SET_TEMP,
    FAN_SPEED,
    HEATER_POWER,
    NIGHT_MODE,
    SERVICE_MODE,
};

Jeżeli chcemy stworzyć funkcję wypisującą odpowiedni label w zadanym języku – nie ma problemu:

void print_label(enum lang lang, enum label_id id)
{
    printf(labels[lang][id]);
}

Jak okiełznać złożoność?

Wszystko zawsze fajnie wygląda na małych przykładach. Potem próbujemy to wykorzystać w większym projekcie i dochodzimy do problemów. A ilość tekstów w takim menu może w końcu rosnąć bardzo szybko. Co wtedy?

Oczywiście w pewnym momencie zamiast przechowywać wszystkie tłumaczenia w jednej tablicy będziemy musieli je jakoś podzielić. Na przykład oddzielna tablica dla każdego widoku w menu. Możemy spokojnie zacząć od jednej tablicy, a kiedy przyjdzie odpowiedni moment – zrobić refactoring.

Tak samo tablice tłumaczeń dla każdego języka możemy trzymać w osobnym pliku. Do tego jeszcze jeden plik zawierający tablice tłumaczeń dla wszystkich języków. Jak te pliki zaczną pęcznieć – oczywiście warto dokonać dalszych podziałów.

Taka struktura jest bardzo praktyczna – możemy dodać nowe tłumaczenie kopiując pliki, nanosząc modyfikacje i rozszerzając zbiorczy plik z tłumaczeniami.

Możemy nawet pójść o krok dalej i generować pliki do kolejnych tłumaczeń automatycznie. Operacje wykonywane przy dodawaniu kolejnych języków są bardzo schematyczne. A więc idealne dla skryptu. Możemy taki skrypt napisać sami. Widziałem też rozwiązania komercyjne, gdzie teksty tłumaczeń uzupełnia się w oddzielnym programie, który następnie generuje pliki źródłowe do obsługi tłumaczeń w kodzie.

Podsumowanie

Tak więc jeżeli odpowiednio przygotujemy nasz projekt – dodanie tłumaczenia tekstów wyświetlanych w menu może być całkiem proste w implementacji. Warto tutaj pamiętać o kilku rzeczach. Tworzenie odpowiednich abstrakcji jest potrzebne nie tylko żeby tablice z tłumaczeniami mogły działać, ale również przydaje się, kiedy tekstów do przetłumaczenia mamy dużo i musimy okiełznać złożoność.

Deklarowanie tablic z tłumaczeniami jako const jest bardzo przydatne. W końcu nie będziemy chcieli zmieniać treści tłumaczeń ani zawartości tablic w trakcie działania programu. Dlatego jeżeli kompilator wykona dodatkowe optymalizacje, albo zapisze te dane w pamięci tylko do odczytu – będzie to dla nas na plus.

Jeżeli artykuł się podobał i chcesz podszlifować swoją znajomość języka C – zapraszam do kursu „C dla Zaawansowanych”!