Obsługa wielu języków

migajek

Od dłuższego czasu poszukiwałem rozwiązania które pozwalałoby na tworzenie tłumaczeń programów (oraz obsługi tych tłumaczeń) w łatwy sposób... Łatwy- czyli z pominięciem setek lini kodu odpowiedzialnych za ładowanie kolejnych napisów. Czyli pliki ini i ładowanie z nich na zasadzie

 
Button1.Caption := INI.ReadString('Form1','Button1','')

odpadają ;)
Oczywiście następnym krokiem ewolucji byłoby szukanie komponentów po nazwach... ale tu pozostaje problem rzutowania typu, aby móc przypisać coś do caption musimy napisać

TButton(Form1.FindComponent(NazwaKomponentu)).Caption := ... 

a przecież nie o to mi chodzi... Dlatego też kombinowałem dalej (jak by se tu życie uprościć) ... i wykombinowałem.
Udało mi się stworzyć system wczytywania który umożliwia na przypisanie praktycznie każdej właściwości do każdego typu komponentu (czyli w przypadku komponentu, który zamiast Caption ma np. MyCaption nie sprawia problemu :) )
Format plików z językami to:
Form1|Button1|Caption=To działa!|Hint=Hint też działa|ShowHint=true
Ze względu na czasochłonne i męczące pisanie "ręczne" plików z językiem, oraz jeszcze gorsze i nieużyteczne (w przypadku dodawania nowych komponentów) rozwiązanie z generowanie plików języka przez odpowiednią procedurę, zdecydowałem się na napisanie plugina do delphi. Zaznaczamy wybrane komponenty, klikamy PMM i w schowku powinien pojawić się kod. Więcej w readme dołączonego pliku (binarka + source + readme):
http://4programmers.net/File:multilang_generator.rar.

Teraz po kolei omówię pola (oddzielone znakiem | )
Pierwsze dwa to pola obowiązkowe!
Pole numer jeden, przechowuje nazwę formy - parenta
Pole numer dwa przechowuje nazwę obiektu który będziemy teraz męczyć ;)
Pozostałe pola są w formacie NazwaWłaściwości=Wartośc
Pól tych może być dowolna ilość

Dopisano: co do form tworzonych po starcie aplikacji, czyli np.

var
  TF : TForm2;
begin
  TF := TForm2.Create(Application);

nalezy pamiętać aby tworzyć je jako childy aplikacji, inaczej tłumaczenie nie obejmie tej formy!

Za przejście do następnego obiektu uważa się koniec lini.

Ważne: wszelkie spacje nie są usuwane, dlatego zapis Form1 | Button1 | Caption =Costam| Hint=nic jest nieprawidłowy!!

W pliku języka można też przechowywać informacje o nazwie języka, jego twórcy, stronie internetowej twórcy i jego adresie e-mail.
przykład umieszczenia informacji w pliku:

#Author=Michał Gajek
#Language=Polski
#Website=http://www.migajek.com
#Email=migajek [zgadnij] yahoo.com

wszystkie inne linie zaczynające się znakiem komentarza ( '#' ) są ignorowane.

Poniżej przedstawię listing pliku MultiLanguage.pas oraz omówię jego funkcje (reszta jest [lub nie ;)] w komentarzach).

(*--------------------------------------------|
| MultiLanguage System Unit                   |
|---------------------------------------------|
| author: Michał Gajek                        |
| web: http://www.migajek.com                 |
| email: migajek[...]yahoo[...]com            |
| Copyright ?  2005 by Michał Gajek           |
| Update and corrections: woolfik@gmail.com   |
|---------------------------------------------|
| Released under the terms and conditions of  |
| the GNU General Public License (Version 2)  |
+--------------------------------------------*)

{
Version 1.5
 Dodana obsluga form dynamicznych
 Dodana obsluge subobiektow (childow) w komponentach
 Poprawiona obsluge klas subobiektow
}


unit MultiLanguage;

interface

uses Windows, Classes, TypInfo, Forms, SysUtils, Dialogs, IniFiles, Controls, DBGrids;

type
  TLanguageFileInfo = record //informacje o pliku jezyka
   Lang : string ; //nazwa jezyka
   Author,www,email:string; //dane o autorze
  end;

  TProperty = record //wlasciwosc obiektu
   PropName:string; //nazwa wlasciwosci, np caption lub hint
   PropValue: string; //wartosc wlasciwosci
  end;

  TLangObject = record // jeden obiekt  do przetlumaczenia
   FormName:string; //nazwa formy-parenta
   ObjName: string; //nazwa obiektu
   Properties : array of TProperty; //parametry
  end;

  TLanguage = record //jezyk, z pliku
   FileInfo:TLanguageFileInfo; //informacje o pliku
   FileName: string; // nazwa pliku
   LangObjects: array of TLangObject; //obiekty do zmiany
  end;

const
 CommentChr : Char = '#';
 BreakLineSign : String[2] = '\n';
 ClassSep : Char = '.';

var
  MainLang:TLanguage;
  plikINI: TMemIniFile;

  procedure LoadLangFromFile(FileName: String); //laduje jezyk do pamieci
  procedure SetLang(OnlyFormName: String = ''); //ustawia (stosuje) zmiany z pliku jezyka

  function Translate(Sekcja, Text: String): String;

  function GetFileInfo(FName: String): TLanguageFileInfo; //zwraca informacje o pliku

  function GenerateLanguageFile(Form: TForm): TStringList; //generuje plik jezyka, dzieki temu nie zapomniy o zadnym komponencie

implementation

{==============               FUNKCJE WEWNETRZNE         ===================}

function GetPropValue(S: String; Separator: String = '='): String; //pobiera wartosc wlasciwosci
begin
  Result := StringReplace(Copy(S, Pos(Separator, S) + 1, Length(S)), BreakLineSign, #10,  [rfReplaceAll]); //zamien przy okazji znak \n na #10
end;

function GetPropName(S: String; Separator: String = '='): string; //pobiera nazwe wlasciwosci
begin
  Result := Copy(S, 1, Pos(Separator, S) - 1);
end;

function GetCommentValue(TS: TStringList; Prop: String): String; //pobiera dane z komentarza, np. #author=jan kowalski
var
  I: Integer;
begin
  Result := '';
  if TS = nil then exit;

  for I := 0 to TS.Count - 1 do
  begin
    if Pos(CommentChr + Prop, TS[I]) > 0 then //jesli w tej lini jest #nazwa_wlasciwosci
    begin
      Result := GetPropValue(Copy(TS[I], Pos(CommentChr, TS[I]), Length(TS[I])));//zwroc sama wartosc
      Exit;
    end;
  end;
end;
{============== KONIEC FUNKCJI WEWNETRZNYCH ================}

{======= funkcja laduje plik do pamieci rozdzielajac dane do zmiennych===========}

procedure LoadLangFromFile(FileName:string);
var
  TS, Line: TStringList; //ts: plik ; line:lista po explode
  I, J: Integer; //zmienne do petli
begin
  PlikINI := TMemIniFile.Create(FileName);
  if not FileExists(FileName ) then Exit; //jak nie ma pliku to won

  ZeroMemory(@MainLang,SizeOf(MainLang)); //wyczyść ;)

  MainLang.FileName := FileName; //ustaw nazwe pliku w rekordzie - czasem przydatne

  TS := TStringlist.Create;  //zawartosc pliku
  Line := TStringList.Create; /// rozbita linia
  TS.LoadFromFile(FileName); //ladowanie

  MainLang.FileInfo := GetFileInfo(FileName); //pobierz informacje o pliku

  for I := Ts.Count - 1 downto 0 do //usuwanie komentarzy
   begin
    if (TS.Strings[II] <> '') and (TS.Strings[I][1] = CommentChr) then
      TS.Delete(I);
   end;

  for I := 0 to TS.Count-1 do
  begin
   Line.Clear;
   ExtractStrings(['|'], [], Pchar(TS[I]), Line); //rozbij linię znakami |
    if not(Line.Count < 2) then //jesli nie jest mniej niz dwie linie po rozbiciu (znaczy jest nazwa formy i obiektu)
     begin
      SetLength(MainLang.LangObjects, Length(MainLang.LangObjects) + 1); //zwieksz pojemnosc tablicy z elementami (obiektami)
      with MainLang.LangObjects[High(MainLang.LangObjects)] do //operacje na najnowszym obiekcie
       begin
        FormName := Line[0]; //ustaw nazwe formy dla obiektu
        ObjName :=   Line[1]; //ustaw nazwe obiektu
        for J := 2 to Line.Count - 1 do //teraz wykonuj operacje na pozostalych wlasciwosciach (nie wiemy ile ich bedzie)
         begin
          SetLength(Properties, Length(Properties) + 1); //ustaw dlugosc tablicy z wlasciwosciami
          Properties[High(Properties)].PropName := GetPropName(Line[J]); //wczytaj nazwe wlasciwosci, np. Caption  (czyli to ci przed znakiem '=' )
          Properties[High(Properties)].PropValue := GetPropValue(Line[J]); //wczytaj wartosc wlasciwosci, np. 'Przycisk 1' (czyli co po zanku '=' );
         end;
       end;
     end;
  end;

  Line.Free; //zwolnij pamiec
  TS.Free; //zwolnij pamiec
end;


{============= ustawia jezyk (wprowadza zmiany na komponentach ==========}

procedure SetLang(OnlyFormName: String = '');
var
  I, J, Z : Integer;
  O: TForm;
  D: TDataModule;
  C: TObject;

  function FindControl(ObjName: String): TObject; //znajduje kontrolke :)
  var
    I, J:integer;
    Ctree: TStringList;
    Obj: TComponent;
  begin
    Result := nil;
    if O <> nil then
    begin
      Ctree := TStringList.Create;
      ExtractStrings([ClassSep],[], PChar(ObjName), Ctree);
      if CTree.Count = -1 then Exit;

      for I := 0 to O.ComponentCount - 1 do
        begin
          if (O.Components[i].Name = Ctree[0]) then
            begin
              Obj := O.Components[i];
              for J := 1 to Ctree.Count - 1 do
                Obj := Obj.FindComponent(Ctree[J]); //szukaj koleujnych, oddzielonych kropka
              Result := Obj;
              Break;
              Exit;
            end;
        end;
      CTree.free;
    end
    else
      if F <> nil then
      begin
        Ctree := TStringList.Create;
        ExtractStrings([ClassSep], [], PChar(ObjName), Ctree);
        if CTree.Count = -1 then Exit;
          for I := 0 to D.ComponentCount - 1 do
          begin
            if (D.Components[I].Name = Ctree[0]) then
              begin
                Obj := D.Components[I];
                for J := 1 to Ctree.Count - 1 do
                  Obj := Obj.FindComponent(Ctree[J]); //szukaj koleujnych, oddzielonych kropka
                Result := Obj;
                Break;  // <=====  WTF is that? o.O   ['mój dopisek']
                Exit;   // <===== najpierw break a potem Exit? exit sie nie wywoła.
              end;
          end;
        CTree.free;
      end
    else
      Exit;
  end;

  procedure SetValue(obj:TObject;PName:string;PValue:string);
  var
    TS: TStringList;
    Items: TStrings;
    I: Integer;
    Col : TCollection ;
    O: TObject;
  begin
    TS := TStringList.Create;
    ExtractStrings(['.'], [], Ochar(PName), TS);

    O := Obj;
    for I := 0 to TS.Count - 2 do
    begin
     if TypInfo.IsPublishedProp(O, TS[I]) then
       O := TypInfo.GetObjectProp(O, TS[I]);
    end;
    if TypInfo.IsPublishedProp(O, TS[TS.count - 1]) then //jesli wlasciwosc istnieje
     if TS[TS.Count - 1] = 'Items' then
       begin
         //i := (o as TCustomListControl).ItemIndex ;
         items := typinfo.GetDynArrayProp(o,ts[ts.count-1]);
         items.DelimitedText := PValue ;
        // (o as TCustomListControl).ItemIndex := i ;
       end
     else if ts[ts.count-1] = 'Columns' then
        begin
          col := (typinfo.GetObjectProp(o,ts[ts.count-1]) as TCollection) ;
          items := TStringList.Create;
          try

            items.DelimitedText := PValue ;
            for i := 0 to items.Count - 1 do
              if col.Count >= i then
                if typinfo.IsPublishedProp(col.Items[i], 'Caption') then
                   typinfo.SetPropValue(col.Items[i], 'Caption', items[i]) // np Listview
                else
                 if typinfo.IsPublishedProp(col.Items[i] , 'Title') then
                   (typinfo.GetObjectProp(col.Items[i] , 'Title') as TColumnTitle).Caption := items[i] //np DBGrid
          finally
            items.Free;
          end;
        end
     else
       typinfo.SetPropValue(o,ts[ts.count-1],PValue); //ustaw wlasciwosc
    ts.Free;
  end;

begin
  for i:=Low(MainLang.LangObjects) to High(MainLang.LangObjects) do
  begin
    with MainLang.LangObjects[i] do
    begin
      if (OnlyFormName='') or (lowercase(OnlyFormName) = lowercase(FormName)) then
      begin //jesli nazwa obecna jest rowna nazwie oczekiwanej to idz dalej
        for z := 0 to Screen.FormCount - 1 do
          if Screen.Forms[z].Name = FormName then
            o := Screen.Forms[z];

        if o = nil then
        begin
          d:=(Application.FindComponent(FormName) as TDataModule);
          if d = nil then
            exit;
        end;

        if FindControl(ObjName) <> nil then
        begin
          c:=FindControl(ObjName);
          for j:=low(Properties) to high(Properties) do //pobieraj wlasciwosci
          begin
            SetValue(c,Properties[j].PropName,Properties[j].PropValue);
          end;
        end;
      end;
   end;
  end;
end;

{=============== POZOSTALE TLUMACZENIA =================}
function Translate(sekcja, text: string): string;
begin
  if plikINI <> nil then
    Result := plikINI.ReadString(sekcja, text, text);
end;
{=======================================================}

{================zwraca informacje o pliku =============}

function GetFileInfo(FName:string):TLanguageFileInfo; //zwraca informacje o pliku
var
  ts:TStringList;
begin
  if not fileexists(FName) then exit;

  ts:=TStringList.Create;
  ts.LoadFromFile(Fname);

  result.Lang:=GetCommentValue(ts,'Language');
  result.Author:=GetCommentValue(ts,'Author');
  result.Www:=GetCommentValue(ts,'Website');
  result.email:=GetCommentValue(ts,'Email');

  ts.Free;
end;


//generuje plik jezyka, dzieki temu nie zapomniy o zadnym komponencie
function GenerateLanguageFile(Form:TForm):TStringList;
var
  items : tstrings ;
  col : TCollection ;
  i,j,k,cnt:integer;
  PropList:PPropList; // lista wlasciwosci :: typinfo.dcu
  PropName:string; //tymczasowa zmienna z nazwa wartosci . UWAGA!!! Lowercase!!
  PropTxt:string;
  ts:string ;
begin
  result:=TStringList.Create;
  result.Clear;
  if Form=nil then exit;

  for i:=0 to Form.ComponentCount-1 do
  begin
    cnt:=GetPropList(Form.Components[i],PropList);// zwraca liczbe wlasciwosci, a do prolist: liste wlasciwosci
    PropTxt:='';
    for j:=0 to cnt-1 do
    begin
      PropName := lowercase(PropList[j].Name); //pobierz tymczasowa zmienna
      if (PropName = 'text')or(propname='caption')or(propName='hint') then
       if typinfo.GetPropValue(Form.Components[i],PropList[j].Name) <> '' then
        PropTxt:=PropTxt+'|'+PropList[j].Name+'='+typinfo.GetPropValue(Form.Components[i],PropList[j].Name);
      if (PropName='items') then
       if typinfo.GetDynArrayProp(Form.Components[i],PropList[j].Name) <> nil then
         begin
           items := typinfo.GetDynArrayProp(Form.Components[i],PropList[j].Name) ;
           try
            PropTxt:=PropTxt+'|'+PropList[j].Name+'='+items.DelimitedText ;
           except
           end;
         end;
      if (PropName='columns') then
       if typinfo.GetDynArrayProp(Form.Components[i],PropList[j].Name) <> nil then
         begin
           col := (typinfo.GetObjectProp(Form.Components[i],PropList[j].Name) as TCollection) ;
           if col.Count > 0 then
             begin
               ts := '"'+col.Items[0].DisplayName+'"' ;
               for k := 1 to col.Count-1 do
                 ts := ts+',"'+col.Items[k].DisplayName+'"'
             end ;
              PropTxt:=PropTxt+'|'+PropList[j].Name+'='+ts ;
           // PropTxt:=PropTxt+'|'+PropList[j].Name+'='+items.DelimitedText ;
         end;

     end;
  if proptxt<>'' then
   result.Text:=result.text+Form.Name+'|'+Form.Components[i].Name+proptxt;
 end;
end;

end.

Obiecałem omówienie funkcji - to obietnicy dotrzymam :)
LoadLangFromFile - ładuje język z pliku podanego w pierwszym parametrze.
SetLang - po załadowaniu języka wprowadza go w życie ;) Jesli parametr jest pusty - funkcja ustawia dane na wszystkich formach... jesli nie jest pusty to ustawia tylko na formie o podanej nazwie... przydatne w przypadku dynamicznie tworzonych form. Oszczednosc czasu w działaniu aplikacji:)

GetFileInfo - po podaniu w parametrze nazwy pliku zwraca rekord z informacjami o nim, czyli nazwa języka i dane o jego autorze.
GenerateLanguageFile - w parametrze podaje się forme dla której chcemy stworzyć plik języka (dzięki temu można zaoszczędzić czas i mieć pewność że żaden komponent nam nie umknie :) ). Funkcja bardzo niedoskonała, narazie generuje tylko parametry Caption, Hint i Text

No cóż to chyba wszystko, proszę o uwagi i oceny (niezbyt surowe :P )
Sądzę że nad plikiem będe jeszcze pracował - poprawki opublikuję tutaj :)

W załączniku dodaję demo :)

wersja 1.2 : dodano obsluge childow w komponentow, tzn mozna teraz uzyskac dostep do np. LabeledEdit1.SubLabel.Caption. Przykładem zmiany podpisu LabeledEdit jest

wersja 1.3 : poprawiono obsługę subklas, teraz mozżna uzyskac dostęp do przykładowo JvWizardWelcomePage1.Subtitle.Text w następujący sposób:

Form1|JvWizardWelcomePage1|Subtitle.Text=aaa
Form1|LabeledEdit1.SubLabel|Caption=costam

multilang_generator.rar

Niestety autor sie nie odzywał więc postanowiłem dokonać kilku modyfikacji:
wersja 1.4: dodano obsługę MidiChild, TDataModul oraz Items np w Combobox lub Radiogroup
Form1|GadioGroup1|Caption=Pobieraj logi:|Items=Wszystkie,"Tylko nowsze"

Wersja 1.5
Dodana obsługa subobiektow np dbgrid i listview

Wersja 1.6
Czekam na sugestie :)

PS.
Ostatnio spotkałem się z błędem przy obsłudze TListView dlatego warto zwrócić uwagę na ten komponent. Postaram się dodać poprawkę najszybciej jak to będzie mozliwe

1 komentarz

Zapraszam na moją stronę http://www.programistakuty.pl. Tam prezentuje mój darmowy program (bardzo łatwo konfigurowalny) do tworzenia wersji językowych programu. Prezentuje tam wykorzystanie go w delphi oraz dołączam przykładowy projekt. Strona artykułu http://programistakuty.pl/kuty-language-changer/