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
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.


It’s great that you are getting ideas from this piece of writing as well as from our argument made at this place.
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.
Good post. I learn something totally new and challenging on sites
I stumbleupon on a daily basis. It will always
be interesting to read through articles from other authors and practice a little something from their web sites.
Everyone loves it when individuals get together and share views.
Great site, continue the good work!
Hey! This is kind of off topic but I need some guidance from an established blog.
Is it very difficult to set up your own blog? I’m not very techincal but I can figure things out pretty quick. I’m thinking about setting up my own but I’m not sure where to start. Do you have any ideas or suggestions? Thank you
Hello there! This article couldn’t be written any better! Looking through this post reminds me of my previous roommate! He constantly kept talking about this. I’ll forward
this information to him. Pretty sure he’s going to have a great read. Thanks for sharing!
You made some really good points there. I looked on the net for additional information about the
issue and found most people will go along with your views on this site.
Hi there I am so delighted I found your site, I really
found you by error, while I was searching on Yahoo for something
else, Anyhow I am here now and would just like to say
cheers for a tremendous post and a all round exciting blog (I also love the theme/design),
I don’t have time to read through it all at the moment but I have book-marked it and also included your RSS feeds, so
when I have time I will be back to read a great deal more, Please do keep up the awesome
jo.
It’s a shame you don’t have a donate button!
I’d without a doubt donate to this fantastic blog! I suppose for now i’ll settle for book-marking
and adding your RSS feed to my Google account. I look forward
to fresh updates and will share this blog with my Facebook group.
Talk soon!