Zapraszam do kontaktu w wygodnej dla Ciebie formie, w celu ustalenia szczegółów.

Na zapytanie odpowiem w dni robocze, do 12 godzin.

Jeśli wolisz, możesz też zadzwonić: tel. 503 692 370

Witryna poświęcona programowaniu 

RSS
Home Kuty Language Changer

Kuty Language Changer

Chciałem Wam zaprezentować kolejny program przeznaczony dla „developerów” chcących rozbudować swoją aplikację o nową funkcjonalność. Gdy wydajemy programy, które chcemy aby były rozpowszechniane na całym świecie przydało by się umożliwić zmianę języka na inny niż polski czy angielski. Na przeciw wychodzę z Kuty Language Chenger-em. Program wraz z kodem zamieszczonym w Waszych programach daje możliwość tłumaczenia go na różne języki.

Zasada jest prosta, w Waszym programie wywołujecie kilka napisanych przeze mnie procedur, a następnie ładujecie wszystko do Kuty Language Changer, tłumaczycie i znowu wywołujecie moje procedury, które powodują zmianę języka. A więc zaczynamy. Tym razem zacznę od prezentacji kodu, a następnie przedstawię obsługę programu.

Na początku prezentuję Wam mój unit PropertiesINI.pas. Zawarte są w nim wszystkie funkcje potrzebne do odczytu i zapisu języka. Najpierw deklarujemy typy, zmienne i nazwy procedur.

type
  TActionKind = (akAccept, akLeave);

const
  OdstepZagniezdzenia = '.';

function IsNullString(Pole: Variant; ZamiastNull: String): String;

function  HasProp(MyComponent: TComponent; Prop: String): Boolean;
procedure SetProp(MyComponent: TComponent; Prop: String; Value: string);

function ReturnComponentPropertiesINI(Component: TObject; PriorClass: string;
                                        TypeKinds: OleVariant; ActionTypeKinds: TActionKind;
                                        Properties: OleVariant;  ActionProperties: TActionKind):string;

function ReturnComponentPropertiesINIForForm(Sender: TForm;
                                        TypeKinds: OleVariant;  ActionTypeKinds: TActionKind;
                                        Properties: OleVariant; ActionProperties: TActionKind): WideString;

function SetComponentPropertiesINI(Sender: TForm; Linia: string;
                                       Properties: OleVariant; ActionProperties: TActionKind ): Boolean;

function SetComponentPropertiesINIForForm(Ini_File: string; Sender: TForm;
                                       Properties: OleVariant; ActionProperties: TActionKind): String;

Funkcja poniżej zamienia nam wartość Null na określony ciąg znaków string.

function IsNullString(Pole: Variant; ZamiastNull: String): String;
begin
  Result := ZamiastNull;
  if VarIsNull(Pole) then
    Result := ZamiastNull
  else
    Result := VarToStr(Pole);
end;

Funkcja sprawdzająca czy dany komponent posiada określone property znajduje się poniżej.

Function HasProp(MyComponent: TComponent; Prop: String): Boolean;
var
  PropInfo: PPropInfo;
begin
  PropInfo := GetPropInfo(MyComponent.ClassInfo, Prop);
  Result := (PropInfo <> NIL);
end;

SetProp – bardzo ważna procedura dzięki której będziemy ustawiać dane tłumaczenie do naszych kontrolek. Korzysta ona z Delphi RTTI (Run Time Type Information) .

procedure SetProp(MyComponent: TComponent; Prop: String; Value: string);
var
  PropInfo: PPropInfo;
//  Bmp: TBitmap;
  LStrs: TStrings;
begin
  try
    PropInfo := GetPropInfo(MyComponent.ClassInfo, Prop);
    if PropInfo <> NIL then
    begin
      if PropInfo.PropType^.Kind <> tkClass then
      begin
        SetPropValue(MyComponent, Prop, Value);
      end else
      begin
        if PropInfo.PropType^^.Name = 'TStrings' then
        begin
          LStrs := TStringList.Create;
          try
            ExtractStrings([';'],[' '],PAnsiChar(Value),LStrs);
            SetObjectProp(MyComponent, Prop, LStrs);
          finally
            FreeAndNil(LStrs);
          end;
        end;
      end;
    end;
  except

  end;
end;

Funkcja zwracająca właściwości danego komponentu z pominięciem właściwości metod.
Component – komponent dla którego właściwości chcemy otrzymać
PriorClass – jest to nazwa tego komponentu
TypeKinds – typy property
ActionTypeKinds – akAccept/akLeave – akceptuj/pomiń typy podane w TypeKinds
Properties – nazwy property
ActionProperties – akAccept/akLeave – akceptuj/pomiń te property podane w Properties

function ReturnComponentPropertiesINI(Component: TObject; PriorClass: string;
                                        TypeKinds: OleVariant;  ActionTypeKinds: TActionKind;
                                        Properties: OleVariant; ActionProperties: TActionKind ):string;
var
  ObjectData: PTypeInfo;
  ObjectInfo: PTypeData;
  PropList: PPropList;
  i, j, k: Integer;
//  LStrList: TStringList;
  Obiekt: TObject;

  Linia: string;
  AkceptujTypeKind, AkceptujWlasciwosc: Boolean;
begin
  Result :='';
  ObjectData := Component.ClassInfo;
  ObjectInfo := GetTypeData(ObjectData);
  GetMem(PropList, SizeOf(PPropInfo) * ObjectInfo.PropCount);
  try
    GetPropInfos(Component.ClassInfo, PropList);
    for i := 0 to ObjectInfo.PropCount - 1 do
      if not (PropList[i]^.PropType^.Kind = tkMethod) then // odrzucenie wszystkich wlasciwosci zdarzeniowych
      begin
        if (PropList[i]^.PropType^.Kind <> tkClass) then
        begin
          if VarIsNull(Properties) then
            AkceptujWlasciwosc:= True
          else
          begin
            AkceptujWlasciwosc:= not (ActionProperties = akAccept);
            for j:=0 to VarArrayHighBound(Properties, 1) do
            begin
              if UpperCase(PropList[i]^.Name) = UpperCase(Properties[j]) then
              begin
                AkceptujWlasciwosc:= (ActionProperties = akAccept);
                Break;
              end;
            end;
          end;

          if AkceptujWlasciwosc then
          begin
            if VarIsNull(TypeKinds) then
              AkceptujTypeKind:= True
            else
            begin
              AkceptujTypeKind:= not (ActionTypeKinds = akAccept);
              for k:=0 to VarArrayHighBound(TypeKinds, 1) do
              begin
                if PropList[i]^.PropType^.Kind = TypeKinds[k] then
                begin
                  AkceptujTypeKind:= (ActionTypeKinds = akAccept);
                  Break;
                end;
              end;
            end;

            if AkceptujTypeKind then
            begin
              Linia := PriorClass+OdstepZagniezdzenia+PropList[i]^.Name + '='
                            + VarToStr(GetPropValue(Component,PropList[i]^.Name));
              if Linia <> '' then
                Result := Result + IfThen(Result <> '', sLineBreak, '') + Linia;

            end;
          end;
        end else
        begin
          Obiekt := GetObjectProp(Component,PropList[i]^.Name);
          if Obiekt <> nil then
          begin
            Linia := ReturnComponentPropertiesINI(Obiekt,
                      PriorClass+OdstepZagniezdzenia + PropList[i]^.Name,
                      TypeKinds, ActionTypeKinds, Properties, ActionProperties);
            if Linia <> '' then
              Result := Result + IfThen(Result <> '', sLineBreak, '') + Linia;
          end;
        end;
      end;
  finally
    FreeMem(PropList, SizeOf(PPropInfo) * ObjectInfo.PropCount);
  end;
end;

ReturnComponentPropertiesINIForForm jest funkcją korzystającą z funkcji zamieszczonej wyżej. Zwraca ona właściwości dla całej formy.

function ReturnComponentPropertiesINIForForm(Sender: TForm;
                                        TypeKinds: OleVariant;  ActionTypeKinds: TActionKind;
                                        Properties: OleVariant; ActionProperties: TActionKind): WideString;
var
  i: Integer;
  Linia: string;
  LStrLista: TStringList;
begin
  Result := '';
  LStrLista := TStringList.Create;
  try
    LStrLista.Add('['+Sender.Name+']');
    Linia := ReturnComponentPropertiesINI(Sender,
              Sender.Name,
              TypeKinds , ActionTypeKinds,
              Properties, ActionProperties);
    if Linia <> '' then
       LStrLista.Text:= LStrLista.Text + Linia;

    for i:=0 to TComponent(Sender).ComponentCount - 1 do
    begin
      Linia := ReturnComponentPropertiesINI(Sender.Components[i],
                Sender.Components[i].Name,
                TypeKinds, ActionTypeKinds,
                Properties, ActionProperties);
      if Linia <> '' then
         LStrLista.Text := LStrLista.Text + Linia;
    end;
    LStrLista.Sort;

    Result := LStrLista.Text;
  finally
    FreeAndNil(LStrLista);
  end;
end;

Procedury poniższe służą do przypisywania wartości właściwością. Pierwsza SetComponentPropertiesINI ustawia właściwość według linii podanej w parametrze wejściowym. Parametr Linia może przyjmować taką wartość: Button1.caption=to jest caption button1. Resztę parametrów wykorzystujemy w ten sam sposób.

function SetComponentPropertiesINI(Sender: TForm; Linia: string;
            Properties: OleVariant; ActionProperties: TActionKind ): Boolean;
var
  LComponentProperty, LValue: string;
  LPosRownaSie: Integer;
  LLength:Integer;

  AkceptujWlasciwosc: Boolean;

  i, j:Integer;
  LStrListProperty: TStringList;
  LPropertyName: string;
  LObiekt: TObject;

begin
  Result := False;
  LStrListProperty := TStringList.Create;
  try
    LPosRownaSie        := Pos('=', Linia);
    LLength             := Length(Linia);
    LComponentProperty  := Copy(Linia, 1,LPosRownaSie-1);
    LValue              := Copy(Linia, LPosRownaSie+1, LLength - LPosRownaSie);

    ExtractStrings([OdstepZagniezdzenia], [' '], PAnsiChar(LComponentProperty), LStrListProperty);
    if LStrListProperty.Count       Exit;

    LObiekt := Sender.FindComponent(LStrListProperty[0]);
    if LObiekt = nil then
      Exit;

    for i:=1 to LStrListProperty.Count - 1 do
    begin
      LPropertyName := LStrListProperty[i];
      if i < LStrListProperty.Count - 1 then
      begin
        LObiekt := GetObjectProp(LObiekt, LPropertyName);
      end else
      begin
        if VarIsNull(Properties) then
          AkceptujWlasciwosc:= True
        else
        begin
          AkceptujWlasciwosc:= not (ActionProperties = akAccept);
          for j:=0 to VarArrayHighBound(Properties, 1) do
          begin
            if UpperCase(LPropertyName) = UpperCase(Properties[j]) then
            begin
              AkceptujWlasciwosc:= (ActionProperties = akAccept);
              Break;
            end;
          end;
        end;

        if AkceptujWlasciwosc then
        begin
          SetProp(TComponent(LObiekt), LPropertyName, IsNullString(LValue,''));
        end;
      end;
    end;
    Result := True;
  finally
    FreeAndNil(LStrListProperty);
  end;
end;

Funkcja ustawia właściwości odczytane z pliku o ścieżce „Ini_File” dla formatki „Sender” z akceptacją/pominięciem właściwości podanych w „Properties”.

function SetComponentPropertiesINIForForm(Ini_File: string; Sender: TForm;
                                       Properties: OleVariant; ActionProperties: TActionKind ): string;
var
  LIni: TIniFile;
  LStrList: TStringList;
  i: Integer;
begin
  Result := '';
  if FileExists(Ini_File) then
  begin
    LIni := TIniFile.Create(Ini_File);
    try
      LStrList := TStringList.Create;
      try
        LIni.ReadSectionValues(Sender.Name, LStrList);
        Result := LStrList.Text;
        for i:=0 to LStrList.Count - 1 do
        begin
          SetComponentPropertiesINI(Sender, LStrList[i], Properties, ActionProperties);
        end;
      finally
        FreeAndNil(LStrList);
      end;
    finally
      FreeAndNil(LIni);
    end;
  end else
    ShowMessage('Plik: "'+Ini_File+'" nie istnieje.');
end;

Tu kończy się ten unit. Za pomocą niego napiszemy procedury do wczytywania i zapisywania języka naszej aplikacji.

Procedura SaveLang powoduje zapisanie wszystkich komponentów z wartościami właściwości. Znajduje się tam pewien fragment kodu:

    LmStringContainer:='';
    for i:=0 to mStringContainer.Lines.Count - 1 do
    begin
      if i = 0 then
        LmStringContainer := LmStringContainer + mStringContainer.Lines[i]
      else
        LmStringContainer := LmStringContainer + ';' + mStringContainer.Lines[i]

    end;
    LStrList.Add('mStringContainerTranslated.Lines='+LmStringContainer);

Jest to pewne rozwiązanie problemu z tekstem zawartym w kodzie. Jeżeli chcemy w kodzie programu zamieścić np.

if cos tam then
begin
  showmessage('Jakiś tekst po polsku');
end;

Kładziemy na formatkę dwa komponenty TMemo, nazywamy je mStringContainer, mStringContainerTranslated i ustawiamy im Visible:=false. Każdy tekst który chcemy wstawić w kodzie dodajemy do nowej linii komponentu mStringContainer, a kod zmieniamy na:

if cos tam then
begin
  showmessage(Translate('Jakiś tekst po polsku'));
end;

Funkcja Translate zostanie wytłumaczona troszkę niżej.

Procedura SaveLang zapisuje wszystkie kontolki przez nas oznaczone do pliku. Plik ten będziemy później wykorzystywać do tłumaczenia w Kuty Language Changer.

procedure SaveLang(FilePath: TFileName = 'Polski.Klang');
var
  LStrList: TStringList;
  i: Integer;
  LmStringContainer: string;
begin
  if FilePath = '' then Exit;
  LstrList := TStringList.Create;
  try
    LStrList.Text := ReturnComponentPropertiesINIForForm(self,
                VarArrayOf([tkChar, tkString, tkWChar, tkLString, tkWString]) , akAccept,
                VarArrayOf(['Caption', 'Hint', 'Title', 'Text']), akAccept);
    LmStringContainer:='';
    for i:=0 to mStringContainer.Lines.Count - 1 do
    begin
      if i = 0 then
        LmStringContainer := LmStringContainer + mStringContainer.Lines[i]
      else
        LmStringContainer := LmStringContainer + ';' + mStringContainer.Lines[i]

    end;
    LStrList.Add('mStringContainerTranslated.Lines='+LmStringContainer);
    LStrList.SaveToFile(FilePath);
  finally
    FreeAndNil(LstrList);
  end;
end;

Procedura LoadLang służy do wczytywania podanego ścieżką pliku języka.

procedure LoadLang(FilePath: TFileName = 'Polski.Klang');
begin
  if FilePath = '' then Exit;
  SetComponentPropertiesINIForForm(FilePath, self,
    {VarArrayOf(['Caption', 'Hint', 'Title', 'Text'])}null, akAccept );
  if Assigned(FStringContainer) then
  begin
    FStringContainer.Clear;
    FStringContainer.AddStrings(mStringContainer.Lines);
  end;
  if Assigned(FStringContainerTranslated) then
  begin
    FStringContainerTranslated.Clear;
    FStringContainerTranslated.AddStrings(mStringContainerTranslated.Lines);
  end;
end;

Translate jest ważną funkcją pozwalającą wstawiać tekst w kod, a nie tylko w komponenty. Częściowo zasada została wytłumaczona wyżej, ale wytłumaczę co to są FStringContainer, FStringContainerTranslated. Obie zmienne to TStringList które mają identyczne Lines z komponentów TMemo leżących na formatce (FStringContainer z mStringContainer, FStringContainerTranslated z mStringContainerTranslated). Pierwsza zawiera zapisane przez nas teksty które wykorzystujemy w kodzie, a druga tłumaczenia załadowane w pliku.

function Translate(StrIn: string): string;
var
  i: Integer;
  Lstr: string;
  LMin: Integer;
begin
  Result := StrIn;
  LMin := min (FStringContainer.Count, FStringContainerTranslated.Count);
  for i:= 0 to LMin - 1 do
  begin
    Lstr := Trim(FStringContainer[i]);
    if (Lstr <> '') and (Lstr = Trim(StrIn)) then
    begin
      Result:= FStringContainerTranslated[i];
      Break;
    end;
  end;
end;

Jak poniższy kod wykorzystać w naszym programie?
Do naszego projektu załączamy plik PropertiesIni.pas. Ustawiamy globalną zmienną widzianą w całym programie do której przypisana jest ścieżka naszego języka niech będzie to FJezykSciezkaPliku: string;. Gdzieś w programie zamieszczamy możliwość wyboru języka i ustawiamy ścieżkę do FJezykSciezkaPliku. W każdej formatce która ma być tłumaczona piszemy

procedure .FormCreate(Sender: TObject);
begin
  FStringContainer := TStringList.Create;
FStringContainerTranslated := TStringList.Create;
  if FJezykSciezkaPliku <> '' then
    LoadLang(FJezykSciezkaPliku);
end;

procedure .FormClose(Sender: TObject; var Action: TCloseAction);
begin
  if FZapiszJezykPodstawowy then
  begin
    if not FileExists(ExtractFilePath(Application.ExeName)+'\language\zapis_formatek\') then
        ForceDirectories(ExtractFilePath(Application.ExeName)+'\language\zapis_formatek\');
    SaveLang(ExtractFilePath(Application.ExeName)+'\language\zapis_formatek\'+Self.Name + '_'+'Polski.Klang');
  end;
end;

procedure .FormDestroy(Sender: TObject);
begin
FreeAndNil(FStringContainer);
FreeAndNil(FStringContainerTranslated);
  if FZapiszJezykPodstawowy then
  begin
    if not FileExists(ExtractFilePath(Application.ExeName)+'\language\zapis_formatek\') then
        ForceDirectories(ExtractFilePath(Application.ExeName)+'\language\zapis_formatek\');
    SaveLang(ExtractFilePath(Application.ExeName)+'\language\zapis_formatek\'+Self.Name + '_'+'Polski.Klang');
  end;
end;

FZapiszJezykPodstawowy jest to również zmienna globalna, która określa czy po zamknięciu bądź zniszczeniu formatki mają zostać zapisane do pliku wszystkie kontrolki z tekstem. Procedura zapisu języka musi być tak umieszczona w kodzie aby dało się ją szybko wyłączyć. Robimy tak ponieważ służy ona tylko jednorazowo w celu zapisu wszytkich kontrolek z tekstem do pliku. Najpierw ustawiamy FZapiszJezykPodstawowy na True, kompilujemy i uruchamiamy. Po uruchomieniu programu otwieramy wszystkie formatki i zamykamy. Wtedy w folderze „\language\zapis_formatek\” zapisane są pliki z nazwami formatek w których są interesujące nas dane. Tworzymy nowy plik z nazwą naszego języka i wklejamy po kolei każdą z formatek. Tak zapisany plik możemy zastosować do tłumaczenia.

Chciałem nadmienić, że najlepszym sposobem jest utworzenie formatki wzorcowej z całą tą obsługą, a kolejne formy powinny dziedziczyć po wzorcu. Unikniemy wtedy powielania kodu na wszystkich formatkach, a w razie zmiany nie musimy dokonywać ich we wszystkich formatkach. Wzorcowa formatka jest do pobrania na końcu artykułu.

Obsługa programu Kuty Language Changer.

1. Najpierw wczytujemy plik, który utworzyliśmy z zapisanych wszystkich formatek. Będzie to nasz bazowy język.

2a. Jeżeli wcześniej zapisaliśmy jakieś tłumaczenie to możemy je odczytać i edytować.

2b. Jeżeli jest to nasze pierwsze tłumaczenie dodajemy nowe tłumaczenie. Podajemy nazwę i wciskamy Ok.

3. Na górze mamy box-a Select Form w którym wybieramy formatkę, którą chcemy tłumaczyć. Na grid-zie wpisujemy nasze tłumaczenie i przechodzimy po kolei.

Obok wyboru formatki mamy możliwość zaznaczenia dwóch checkbox-ów.

„Show only suplemented” – po zaznaczeniu pokazuje tylko kontrolki, które mają uzupełniony tekst. Jest to przydatne w sytuacji gdy mamy dużo kontrolek które nie mają ustawionych np. hintów więc nie ma potrzeby ich wyświetlać.

„Refresh application” – jeżeli obsłużymy tą opcję w naszej aplikacji (opis jak to zrobić w osobnym artykule) to po włączeniu jej, po zmianie tłumaczenia, informacja zostanie wysłana do aplikacji którą tłumaczymy i komponent zmieni właściwość na tą z tłumaczenia. Jedyne ograniczenie jest dla długości znaków. Powyżej 120 znaków property się nie zmieni. Jest to przydatne ponieważ na żywo widzimy jak nasza aplikacja wygląda po przetłumaczeniu i czy nowy ciąg znaków mieści się w kontrolce. Dodatkowo jeżeli ilość znaków tłumaczenia przekroczy ilość z wersji podstawowego języka, pole zostanie podświetlone kolorem czerwonym.

4. Jeżeli jest to długi tekst (np. przełamany enterami) możemy go edytować za pomocą tej opcji. Najczęściej jest wykorzystywane do edytowania tekstu z mStringContainerTranslated, które pozwala na tłumaczenie tekstu znajdującego się w kodzie.

5. Możemy również dodawać ręcznie nowe property (w sytuacji niedodania się automatycznie, w sytuacji wcześniejszego usunięcia itp.)

Teraz opiszę pozostałe opcje.

„Delete item” powoduje usunięcie aktywnej pozycji.

„Del unaccompanied” usuwa wszystkie puste property.

„Clear all list” powoduje wyczyszczenie listy. Można na nowo wczytywać i tłumaczyć.

„Save translation” powoduje zapisanie aktualnego tłumaczenia.

Po naciśnięciu przycisku „Start” rozpoczyna się odczyt kontrolek spod kursora myszy. Informacje pojawiają się w „Control Name” i „Control text”.

Liczę, że w miarę jasno wytłumaczyłem w jaki sposób wykorzystywać program do własnych celów.

Poniżej zamieszczam linki do kodów źródłowych

Pobierz: Kuty Language Demo

Przejdź do sekcji Pobieranie i pobierz program.

Aplikacja wykorzystuje mechanizm komunikacji między aplikacjami. Przeczytaj Artykuł: Komunikacja między aplikacjami aby wiedzieć jak wykorzystać tą funkcjonalność. W skrócie: wystarzczy załączyć Unit KutyKomunikacjaAplikacji.pas do projektu której główna forma nazywa się frmGlowny i obsługa jest zrobiona. W Kuty Language Changer zaznaczamy checkbox-a Refresh Application, uruchamiamy swoją aplikację i w momęcie wpisania tłumaczenia dla danej kontrolki automatycznie zostanie ta kontrolka odświeżona. Jest to przydatne, ponieważ od razu widzimy jak aplikacja jest tłumaczona i czy dane tłumaczenie mieści się w kontrolce z pierwotnym tekstem.

2 komentarze

  1. abbiemiranda@gmail.com

    It’s great that you are getting ideas from this piece of writing as well as from our argument made at this place.

  2. Katherin Branham

    Hello there! I just would like to give you a huge thumbs up for your great info you have here on
    this post. I am returning to your blog for more soon.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *