Naciśnij “Enter” aby skoczyć do treści

Tiny Radio Player #06 – Zmiana języka aplikacji

Istnieje wiele sposobów na zarządzanie językiem aplikacji. Najpopularniejszym jest chyba wbudowany mechanizm i18n, który przy przebudowie aplikacji zapisuje wszystkie stringi do plików .po. Pliki takie możemy następnie tłumaczyć na inne języki. Wykorzystujemy do tego zewnętrzne narzędzia, z których chyba najbardziej znanym jest edytor poedit. Jeżeli taki sposób Ci odpowiada to możesz włączyć obsługę i18n z poziomu opcji projektu Project->Project Options->i18n.

Ja natomiast chciałbym przedstawić inny sposób, gdzie utworzymy własny mechanizm umożliwiający zapis i odczyt przetłumaczonych pozycji z pliku xml. Przykładowy plik z tłumaczeniem na język polski może wyglądać mniej więcej tak

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Resource>
	<Items language="PL" author="Jakub Kurlowicz" version="0.1" date="2018-10-14" url="jakubkurlowicz.pl" email="jkurlowicz@gmail.com">
		
		<!-- Error Messages -->
		<Item name="ErrorMessage.UnspecifiedError" type="string">Wystąpił nieznany błąd!</Item>
		
		<!-- General Buttons -->
		<Item name="Button.Close" type="string">Zamknij</Item>
		<Item name="Button.Cancel" type="string">Anuluj</Item>
		<Item name="Button.Save" type="string">Zapisz</Item>
		<Item name="Button.Delete" type="string">Usuń</Item>
		<Item name="Button.Open" type="string">Otwórz</Item>
		
		<!-- Main Menu -->
		<Item name="MainMenu.File" type="string">Plik</Item>
		<Item name="MainMenu.File.Exit" type="string">Zakończ</Item>
		<Item name="MainMenu.Settings" type="string">Ustawienia</Item>
		<Item name="MainMenu.Settings.Language" type="string">Język</Item>
		
		<!-- Test -->
		<Item name="Test.Button.Play" type="string">Odtwórz</Item>
		<Item name="Test.Button.Stop" type="string">Zatrzymaj</Item>

	</Items>
</Resource>

Wydzielić tu możemy dwie sekcje. W nagłówku podajemy informacje o autorze i wersji tłumaczenia, natomiast w szczegółach pozycje tłumaczeń. Nazwa pozycji jest dowolna i zależy tylko od nas. Pamiętajmy natomiast, że to właśnie tą nazwę będziemy musieli podać chcąc odwołać się do wartości pozycji tłumaczenia.

Do zarządzania językami tworzymy klasę TLanguage (language.pas).

TLanguage = class sealed (TObject)
  private
    // class variables and methods belongs to the class, not to the instance
    class var FLanguageHashmap: TStringList;
    class var FLanguageInfoLanguage: string;
    class var FLanguageInfoAuthor: string;
    class var FLanguageInfoVersion: string;
    class var FLanguageInfoDate: string;
    class var FLanguageInfoUrl: string;
    class var FLanguageInfoEmail: string;

    class var FLanguageChangeEventList: TMethodList;

    class procedure Initialize();
    class procedure Finalize();

    class function GetLanguageResources(const LanguageName: string): Boolean;
    class function GetLanguageFileName(const UseDefaultLanguage: Boolean = false): string;
    class function GetLanguageFilePath(const LanguageName: string): string;

    class procedure SearchAllLanguageFiles();
  public
    class var LanguageFiles: TStringList;
    class var CurrentLangName: string;

    // static methods
    class function Get(const Item: string;
      const DefaultValue: string = EMPTY_STR): string;
    class procedure ReloadLanguageItems();
    class procedure ChangeLanguage(const LanguageName: string);

    class function GetOSLanguage(): string;

    // events
    class procedure RegisterLanguageChangeEvent(const ANotification: TNotifyEvent);
    class procedure UnregisterLanguageChangeEvent(const ANotification: TNotifyEvent);
  end;

Przy starcie aplikacji wczytujemy wszystkie nazwy plików językowych (SearchAllLanguageFiles) i zapisujemy na liście LanguageFiles. Następnie jeżeli jest to pierwsze uruchomienie, próbujemy ustalić język na podstawie języka systemu operacyjnego, jeżeli to się nie uda to ustawiamy domyślny język angielski. Jeżeli jest to kolejne uruchomienie aplikacji to wybrany język odczytujemy z ustawień aplikacji (możesz o tym przeczytać w poprzednim wpisie). Taki zapis w pliku konfiguracyjnym wygląda mniej więcej tak:

<?xml version="1.0" encoding="UTF-8"?>
<Settings>
  <Item name="Volume">100</Item>
  <Item name="SelectedLanguage">language-pl</Item>
</Settings>

Po ustaleniu nazwy pliku z tłumaczeniem (GetLanguageFileName) przechodzimy do wczytania pozycji tłumaczeń (GetLanguageResources). Jeżeli operacja zakończy się sukcesem to daną pozycję tłumaczenia możemy pobrać wywołując metodę Get, np.

btnPlay.Caption := Language.TLanguage.Get('Test.Button.Play');

// możemy również przekazać wartość domyślną, która zostanie zwrócona jeżeli dana pozycja nie zostanie znaleziona
btnPlay.Caption := Language.TLanguage.Get('Test.Button.Play', 'Play');

Aby skrócić zapis odwołania do metody Get napiszemy niewielką metodę pomocniczą, którą umieścimy w pliku Helpers.pas.

// Get language item
function GetLanguageItem(const Item: string; const DefaultValue: string): string;
begin
  Result := Language.TLanguage.Get(item, defaultValue);
end;

Teraz do pozycji tłumaczeń odwołać się możemy w następujący sposób:

btnPlay.Caption := GetLanguageItem('Test.Button.Play');

btnPlay.Caption := GetLanguageItem('Test.Button.Play', 'Play');

Aby zmienić język aplikacji wywołujemy metodę ChangeLanguage. Jako parametr przekazujemy nazwę pliku z tłumaczeniem. Jeżeli dany plik zostanie znaleziony i wczytany to wywołujemy zdarzenie LanguageChangeEvent. Informuje ono wszystkich zapisanych subskrybentów o zmianie języka. Aby się na takie zdarzenie zapisać przekazujemy do RegisterLanguageChangeEvent wskaźnik na lokalną metodę. Zostanie ona wywołana po zmianie języka.

procedure TMainForm.FormCreate(Sender: TObject);
begin
  TLanguage.RegisterLanguageChangeEvent(@OnLanguageChange);
end;

procedure TMainForm.OnLanguageChange(Sender: TObject);
begin
  LoadLoanguages;
end;

procedure TMainForm.LoadLoanguages;
begin
  btnPlay.Caption := GetLanguageItem('Test.Button.Play', 'Play');
  btnStop.Caption := GetLanguageItem('Test.Button.Stop', 'Stop');

  miFile.Caption := GetLanguageItem('MainMenu.File', 'File');
  miExit.Caption := GetLanguageItem('MainMenu.File.Exit', 'Exit');
  miSettings.Caption := GetLanguageItem('MainMenu.Settings', 'Settings');
  miLanguage.Caption := GetLanguageItem('MainMenu.Settings.Language', 'Language');
end;  

Do wypisania się z listy subskrybentów (np. podczas niszczenia formy) służy metoda UnregisterLanguageChangeEvent. Jako parametr przekazujemy wskaźnik wcześniej zarejestrowanej metody.

Na zakończenie warto dodać, że zmianę języka aplikacji można wykonać z poziomu menu głównego. Wcześniej musimy jednak dodać wszystkie języki oraz podpiąć metodę, która będzie wywołana podczas kliknięcia na dany język.

procedure TMainForm.FormCreate(Sender: TObject);
begin
  AddLanguageItems;
end;

procedure TMainForm.AddLanguageItems;
var
  i: integer;
  subItem: TMenuItem;
begin
  for i := 0 to Pred(TLanguage.LanguageFiles.Count) do
  begin
    subItem := TMenuItem.Create(miLanguage);
    subItem.Caption := TLanguage.LanguageFiles[i];
    subItem.Tag := i;
    subItem.OnClick := @miLanguageItemClick;
    subItem.Checked := TLanguage.CurrentLangName = TLanguage.LanguageFiles[i];
    miLanguage.Add(subItem);
  end;
end;

procedure TMainForm.miLanguageItemClick(Sender: TObject);
var
  i: integer;
  mi : TMenuItem;
begin
  if Sender is TMenuItem then
  begin
    // mark the selected item and deselect all others
    mi := TMenuItem(Sender);
    for i := 0 to Pred(mi.Parent.Count) do
      mi.Parent.Items[i].Checked := mi.Parent.Items[i] = mi;

    TLanguage.ChangeLanguage(mi.Caption);
  end;
end;

Efekt końcowy wygląda tak.

 

Kod aplikacji dostępny jest na GitHubie a jeżeli interesują Cię tylko zmiany dotyczące tego wpisu to zapraszam tu.