{----------------------------------------------------------------------------- The contents of this file are subject to the Mozilla Public License Version 1.1 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.mozilla.org/MPL/MPL-1.1.html Software distributed under the License is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either expressed or implied. See the License for the specific language governing rights and limitations under the License. The Original Code is: JvgLanguageLoader.PAS, released on 2003-01-15. The Initial Developer of the Original Code is Andrey V. Chudin, [chudin att yandex dott ru] Portions created by Andrey V. Chudin are Copyright (C) 2003 Andrey V. Chudin. All Rights Reserved. Contributor(s): Michael Beck [mbeck att bigfoot dott com]. Burov Dmitry, translation of russian text. You may retrieve the latest version of this file at the Project JEDI's JVCL home page, located at http://jvcl.sourceforge.net Description: eng: Load new string resources from file to components. Uses RTTI Replaces the found lines(strings) from one language to another if found in dictionary Dictionary is text like: Langauge1_line=Language2_line .. Language1_line=Language2_line rus: Заменяет по словарю из файла все найденные строки одного языка на другой Словарь в виде текста вида: Строка на языке 1=Строка на языке 2 ... Строка на языке 1=Строка на языке 2 Known Issues: -----------------------------------------------------------------------------} // $Id: JvgLanguageLoader.pas 11043 2006-11-26 07:21:48Z marquardt $ unit JvgLanguageLoader; {$I jvcl.inc} interface uses {$IFDEF USEJVCL} {$IFDEF UNITVERSIONING} JclUnitVersioning, {$ENDIF UNITVERSIONING} {$ENDIF USEJVCL} Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, {$IFDEF USEJVCL} ComCtrls, JvComponentBase; {$ELSE} ComCtrls; {$ENDIF USEJVCL} type TLanguageLoaderOptions = set of (lofTrimSpaces, lofConvertEscapes); // lofConvertEscapes: if set (=default) converts escape sequences when reading // language strings form language file, eg \n = #10 (Note: only used in // FTranslationStrings, not in FTranslations //{опция удаления начальных и завершающих пробелов} { Option to Trim first and last spaces [translated] } {$IFDEF USEJVCL} TJvgLanguageLoader = class(TJvComponent) {$ELSE} TJvgLanguageLoader = class(TComponent) {$ENDIF USEJVCL} private // FTranslations: temporary stringlist with translations of strings on a form // Note: is destroyed after the translation is complete! FTranslations: TStringList; // FOldStrings: temporary stringlist needed when changing translations // Note: is destroyed after the translation is complete! FOldStrings: TStringList; // FTranslationChange: used internally. Set to true when changing translations // This way we now we have to look up the original text in FOldStrings. FTranslationChange: Boolean; // FOptions: some translation options FOptions: TLanguageLoaderOptions; // FSaveEmpty doesn't work at the moment? (include in options?) //FSaveEmpty: Boolean; //JGB: save empty strings or not (default=false) // FDictionaryFileName: Name and path of the language dictionary file FDictionaryFileName: string; // FFormSection: section in languagefile where the translation of the // current form can be found // (empty=default=[] (if Owner=nil: [Translation]) FFormSection: string; // FStringsSection: section in languagefile where the translation of // string consts can be found // (empty=default=[.Strings] (if Owner=nil: [Translation.Strings]) FStringsSection: string; // FTranslationStrings: list of string translations! // Read form FStringSection of language file FTranslationStrings: TStringList; // FIgnoreList: List of component names that should not be translated e.g. contents of a RichEdit control FIgnoreList: TStringList; // DEPRECATED function TranslateString(AText: string): string; procedure SetIgnoreList(const Value: TStringList); procedure SetTranslationStrings(const Value: TStringList); function GetFormSection: string; function GetStringsSection: string; protected procedure UpdateComponent(Component: TPersistent); virtual; procedure GetAllComponentStrings(Component: TPersistent); virtual; // Changed to internal function procedure LoadLanguage(Component: TComponent; const FileName: string); public constructor Create(AOwner: TComponent); override; destructor Destroy; override; // TranslateComponent: Translate the said component using the language file // specified in FDictionaryFileName procedure TranslateComponent(Component: TComponent; DoLoadStrings: Boolean); // ChangeTranslation: As TranslateComponent but now change an already // translated component to another language. DictionaryFileName should // contain the new language filename and the old (current) language file // should be in OldLanguageFilename procedure ChangeTranslation(Component: TComponent; DoLoadStrings: Boolean; OldLanguageFileName: string); // LoadStrings: Loads strings form the FStringsSection of the languagefile // into FTranslationStrings, after that it can be used in MessageBox and Translate procedure LoadStrings; // JGB - Save all strings on a form (equivalent to LoadLanguage procedure SaveAllStrings(Component: TComponent; FileName: string); // JGB - Translation wrapper for Application.Messagebox // Call this instead of Application.Messagebox when translation is needed function MessageBox(const Text, Caption: PChar; Flags: Longint = MB_OK): Integer; // Maybe more like MessageBox needed? // JGB - Translate: use this to translate a string loaded in ... stringlist (todo) function Translate(const Text: string): string; property TranslationStrings: TStringList read FTranslationStrings write SetTranslationStrings; published property Options: TLanguageLoaderOptions read FOptions write FOptions default [lofTrimSpaces, lofConvertEscapes]; property DictionaryFileName: string read FDictionaryFileName write FDictionaryFileName; property FormSection: string read GetFormSection write FFormSection; property StringsSection: string read GetStringsSection write FStringsSection; property IgnoreList: TStringList read FIgnoreList write SetIgnoreList; end; procedure LoadLanguage(Component: TComponent; FileName: string; Options: TLanguageLoaderOptions); {$IFDEF USEJVCL} {$IFDEF UNITVERSIONING} const UnitVersioning: TUnitVersionInfo = ( RCSfile: '$URL: https://jvcl.svn.sourceforge.net/svnroot/jvcl/tags/JVCL3_32/run/JvgLanguageLoader.pas $'; Revision: '$Revision: 11043 $'; Date: '$Date: 2006-11-26 08:21:48 +0100 (dim., 26 nov. 2006) $'; LogPath: 'JVCL\run' ); {$ENDIF UNITVERSIONING} {$ENDIF USEJVCL} implementation {.$DEFINE DEBUG_LANG} uses {$IFDEF DEBUG_LANG}DbugIntf,{$ENDIF} TypInfo, IniFiles, JclStrings; //{Ф-ия для загрузки словаря без предварительного создания компонента} { Function to load dictionary without previous creation of the component [translated] } procedure LoadLanguage(Component: TComponent; FileName: string; Options: TLanguageLoaderOptions); var LanguageLoader: TJvgLanguageLoader; begin LanguageLoader := TJvgLanguageLoader.Create(nil); try LanguageLoader.LoadLanguage(Component, FileName); finally LanguageLoader.Free; end; end; { TJvgLanguageLoader } constructor TJvgLanguageLoader.Create(AOwner: TComponent); begin inherited; Options := [lofTrimSpaces,lofConvertEscapes]; FIgnoreList := TStringList.Create; FIgnoreList.Sorted := True; {$IFDEF COMPILER6_UP} FIgnoreList.CaseSensitive := True; {$ENDIF COMPILER6_UP} FTranslationStrings := TStringList.Create; FTranslationStrings.Sorted := True; {$IFDEF COMPILER6_UP} FTranslationStrings.CaseSensitive := True; {$ENDIF COMPILER6_UP} end; destructor TJvgLanguageLoader.Destroy; begin FreeAndNil(FIgnoreList); FreeAndNil(FTranslationStrings); inherited Destroy; end; procedure TJvgLanguageLoader.SetIgnoreList(const Value: TStringList); begin FIgnoreList.Assign(Value); end; procedure TJvgLanguageLoader.SetTranslationStrings(const Value: TStringList); begin FTranslationStrings.Assign(Value); end; function TJvgLanguageLoader.GetFormSection: string; begin if FFormSection <> '' then Result := FFormSection else if Owner <> nil then Result := Owner.Name else Result := 'Translation'; end; function TJvgLanguageLoader.GetStringsSection: string; begin if FStringsSection <> '' then Result := FStringsSection else if Owner <> nil then Result := Owner.Name else Result := 'Translation.Strings'; end; //{ Загрузка словаря, обход указанного компонента и } //{ всех его дочерних компонентов } { Loading dictionary, passing the given component and all his children components [translated] } procedure TJvgLanguageLoader.LoadLanguage(Component: TComponent; const FileName: string); var IniFile: TCustomIniFile; procedure UpdateAllComponents(Component: TComponent); var I: Integer; begin //{ обработка своцств компонента } { Processing the component's properties [translated] } UpdateComponent(Component); for I := 0 to Component.ComponentCount - 1 do UpdateAllComponents(Component.Components[I]); end; begin FTranslations := TStringList.Create; IniFile := TIniFile.Create(FileName); try //{ Загрузка словаря из заданного файла } { Loading dictionary from given file } FTranslations.Sorted := true; {$IFDEF COMPILER6_UP} FTranslations.CaseSensitive := True; {$ENDIF COMPILER6_UP} IniFile.ReadSectionValues(FormSection,FTranslations); UpdateAllComponents(Component); finally FTranslations.Free; FreeAndNil(IniFile); end; end; // TranslateComponent: Translate the said component using the language file // specified in FDictionaryFileName // if DoLoadStrings = true then LoadStrings is called after translating the form procedure TJvgLanguageLoader.TranslateComponent(Component: TComponent; DoLoadStrings: Boolean); begin if DictionaryFileName <> '' then begin LoadLanguage(Component,DictionaryFileName); if DoLoadStrings then LoadStrings; end; end; // ChangeTranslation: As TranslateComponent but now change an already // translated component to another language. DictionaryFileName should // contain the new language filename and the old (current) language file // should be in OldLanguageFilename procedure TJvgLanguageLoader.ChangeTranslation(Component: TComponent; DoLoadStrings: Boolean; OldLanguageFileName: string); var OldIniFile: TCustomIniFile; I: Integer; EqualPos, Len: Integer; TempStr: string; begin if (DictionaryFileName <> '') and (OldLanguageFileName <> '') then begin FOldStrings := TStringList.Create; OldIniFile := TIniFile.Create(OldLanguageFileName); try {$IFDEF COMPILER6_UP} FOldStrings.CaseSensitive := True; {$ENDIF COMPILER6_UP} FOldStrings.Sorted := False; OldIniFile.ReadSectionValues(FormSection, FOldStrings); FOldStrings.BeginUpdate; for I := 0 to FOldStrings.Count-1 do begin TempStr := FOldStrings.Strings[I]; EqualPos := Pos('=', TempStr); if EqualPos > 0 then // Found: exchange Name=Value begin Len := Length(TempStr); FOldStrings.Strings[I] := Copy(TempStr, EqualPos + 1, Len - EqualPos) + '=' + Copy(TempStr,1,EqualPos-1); end; end; FOldStrings.EndUpdate; FOldStrings.Sorted := True; FTranslationChange := True; LoadLanguage(Component, DictionaryFileName); finally FTranslationChange := False; FreeAndNil(FOldStrings); OldIniFile.Free; end; // *** Alas, Changing translation for the following is not possible! if DoLoadStrings then LoadStrings; end; end; // LoadStrings: Loads strings form the FStringsSection of the languagefile // into FTranslationStrings, after that it can be used in MessageBox and Translate procedure TJvgLanguageLoader.LoadStrings; var I: Integer; IniFile: TCustomIniFile; begin if DictionaryFileName <> '' then begin IniFile := TIniFile.Create(DictionaryFileName); try TranslationStrings.Clear; // Remove old translation strings TranslationStrings.Sorted := False; // no sort during processing IniFile.ReadSectionValues(StringsSection, TranslationStrings); if lofConvertEscapes in Options then begin TranslationStrings.BeginUpdate; for I := 0 to TranslationStrings.Count-1 do TranslationStrings.Strings[I] := StrEscapedToString(TranslationStrings.Strings[i]); TranslationStrings.EndUpdate; end; TranslationStrings.Sorted := True; // and now sort once finally FreeAndNil(IniFile); end; end; end; // JGB - Save all strings on a form (equivalent to LoadLanguage procedure TJvgLanguageLoader.SaveAllStrings(Component: TComponent; FileName: string); var IniFile: TCustomIniFile; procedure GetAllComponents(Component: TComponent); var I: Integer; begin // Processing the component's properties [translated] GetAllComponentStrings(Component); for I := 0 to Component.ComponentCount - 1 do GetAllComponents(Component.Components[I]); end; procedure WriteSectionValues; var i: Integer; begin for I := 0 to FTranslations.Count-1 do IniFile.WriteString(FormSection, FTranslations.Names[i],FTranslations.Values[FTranslations.Names[i]]); end; begin FTranslations := TStringList.Create; IniFile := TIniFile.Create(FileName); try // Saving dictionary to given file GetAllComponents(Component); WriteSectionValues; // write to dictionary finally FTranslations.Free; IniFile.Free; end; end; //{ Проход по всем свойствам компонента } //{ Для всех строковых свойств - загрузка перевода из сооваря } { Passing(iterating) all the component's properties For all string properties - load the translation from dictionary [translated] } procedure TJvgLanguageLoader.UpdateComponent(Component: TPersistent); var PropInfo: PPropInfo; TypeInf, PropTypeInf: PTypeInfo; TypeData: PTypeData; I, J, Idx: integer; AName, PropName, StringPropValue: string; PropList: PPropList; NumProps: word; PropObject: TObject; OldSort: Boolean; TempTrans: string; function NeedsTranslation(SourceStr: string; var DestStr: string): Boolean; begin DestStr := TranslateString(SourceStr); Result := SourceStr <> DestStr; end; begin // JGB: First we look if this component is in the ignore list if IgnoreList.Find(Component.GetNamePath, Idx) then Exit; // Found in the list of excluded components so don't translate { Playing with RTTI } TypeInf := Component.ClassInfo; AName := TypeInf^.Name; TypeData := GetTypeData(TypeInf); NumProps := TypeData^.PropCount; GetMem(PropList, NumProps * sizeof(Pointer)); try GetPropInfos(TypeInf, PropList); for I := 0 to NumProps - 1 do begin PropName := PropList^[I]^.Name; PropTypeInf := PropList^[I]^.PropType^; PropInfo := PropList^[I]; case PropTypeInf^.Kind of tkString, tkLString: //{ Переводить свойство Name не следует } { .Name is not to be translated [translated] } if PropName <> 'Name' then begin //{ Получение значения свойства и поиск перевода в словаре } { Retrieving the property's value and searchin for translation in dictionary [translated] } StringPropValue := GetStrProp(Component, PropInfo); if NeedsTranslation(StringPropValue, TempTrans) then SetStrProp(Component, PropInfo, TempTrans); end; tkClass: begin PropObject := GetObjectProp(Component, PropInfo); {, TPersistent} if Assigned(PropObject) then begin //{ Для дочерних свойств-классов вызов просмотра свойств } { For children properties-classes calling iterate again [translated] } if PropObject is TPersistent then UpdateComponent(PropObject as TPersistent); //{ Индивидуальный подход к некоторым классам } { Specific handling of some certain classes [translated] } if PropObject is TStrings then begin TStrings(PropObject).BeginUpdate; try if PropObject is TStringList then begin // It's problematic when a stringlist is sorted: // so reset it for now OldSort := (PropObject as TStringList).Sorted; (PropObject as TStringList).Sorted := False; for J := 0 to (PropObject as TStrings).Count - 1 do if NeedsTranslation(TStrings(PropObject)[J], TempTrans) then TStrings(PropObject)[J] := TempTrans; (PropObject as TStringList).Sorted := OldSort; end else for J := 0 to (PropObject as TStrings).Count - 1 do if NeedsTranslation(TStrings(PropObject)[J], TempTrans) then TStrings(PropObject)[J] := TempTrans; finally TStrings(PropObject).EndUpdate; end; end; if PropObject is TTreeNodes then begin TTreeNodes(PropObject).BeginUpdate; try for J := 0 to (PropObject as TTreeNodes).Count - 1 do if NeedsTranslation(TTreeNodes(PropObject).Item[J].Text, TempTrans) then TTreeNodes(PropObject).Item[J].Text := TempTrans; finally TTreeNodes(PropObject).EndUpdate; end; end; if PropObject is TListItems then begin TListItems(PropObject).BeginUpdate; try for J := 0 to (PropObject as TListItems).Count - 1 do if NeedsTranslation(TListItems(PropObject).Item[J].Caption, TempTrans) then TListItems(PropObject).Item[J].Caption := TempTrans; finally TListItems(PropObject).EndUpdate; end; end; //{ Здесь можно добавить обработку остальных классов } { And here may be added more specific handlers for certain other classes [translated] } end; end; end; end; finally FreeMem(PropList, NumProps * sizeof(Pointer)); end; end; { Get all strings on this component that can be translated and put it in the dictionary. } procedure TJvgLanguageLoader.GetAllComponentStrings(Component: TPersistent); var PropInfo: PPropInfo; TypeInf, PropTypeInf: PTypeInfo; TypeData: PTypeData; I, J, Idx: integer; AName, PropName, StringPropValue: string; PropList: PPropList; NumProps: Word; PropObject: TObject; begin // JGB: First we look if this component is in the ignore list if IgnoreList.Find(Component.GetNamePath, Idx) then Exit; // Found in the list of excluded components so don't translate { Playing with RTTI } TypeInf := Component.ClassInfo; AName := TypeInf^.Name; TypeData := GetTypeData(TypeInf); NumProps := TypeData^.PropCount; GetMem(PropList, NumProps * sizeof(pointer)); try GetPropInfos(TypeInf, PropList); for I := 0 to NumProps - 1 do begin PropName := PropList^[I]^.Name; PropTypeInf := PropList^[I]^.PropType^; PropInfo := PropList^[I]; case PropTypeInf^.Kind of tkString, tkLString: { .Name is not to be translated [translated] } if PropName <> 'Name' then begin { Retrieving the property's value and storing in dictionary } StringPropValue := GetStrProp(Component, PropInfo); if {FSaveEmpty or} (StringPropValue <> '') then begin //FTranslations.Add(PropName+'='+StringPropValue); FTranslations.Add(Component.GetNamePath + '.' + PropName); FTranslations.Values[Component.GetNamePath + '.' + PropName] := StringPropValue; end; end; tkClass: begin PropObject := GetObjectProp(Component, PropInfo); //{, TPersistent} if Assigned(PropObject) then begin { For children properties-classes calling iterate again [translated] } if (PropObject is TPersistent) then UpdateComponent(PropObject as TPersistent); { Specific handling of some certain classes [translated] } if (PropObject is TStrings) then begin for J := 0 to (PropObject as TStrings).Count - 1 do if {FSaveEmpty or} (TStrings(PropObject)[J] <> '') then begin FTranslations.Add(Component.GetNamePath + '.' + PropName); FTranslations.Values[Component.GetNamePath + '.' + PropName] := TStrings(PropObject)[J]; end; end; if (PropObject is TTreeNodes) then begin for J := 0 to (PropObject as TTreeNodes).Count - 1 do if {FSaveEmpty or} (TTreeNodes(PropObject).Item[J].Text <> '') then begin FTranslations.Add(Component.GetNamePath + '.' + PropName); FTranslations.Values[Component.GetNamePath + '.' + PropName] := TTreeNodes(PropObject).Item[J].Text; end; end; if (PropObject is TListItems) then begin for J := 0 to (PropObject as TListItems).Count - 1 do if {FSaveEmpty or} (TListItems(PropObject).Item[J].Caption <> '') then begin FTranslations.Add(Component.GetNamePath + '.' + PropName); FTranslations.Values[Component.GetNamePath + '.' + PropName] := TListItems(PropObject).Item[J].Caption; end; end; { And here may be added more specific handlers for certain other classes [translated] } end; end; end; end; finally FreeMem(PropList, NumProps * sizeof(pointer)); end; end; //{ Поиск перевода для заданной строки в словаре } { Searching for translation of given line in dictionary [translated] } function TJvgLanguageLoader.TranslateString(AText: string): string; var TempStr: string; begin if lofTrimSpaces in Options then AText := trim(AText); if FTranslationChange then TempStr := FOldStrings.Values[AText] else TempStr := AText; if FTranslations.IndexOfName(TempStr) <> -1 then Result := FTranslations.Values[TempStr] else Result := AText; end; // JGB - Translation wrapper for Application.Messagebox // Call this instead of Application.Messagebox when translation is needed function TJvgLanguageLoader.MessageBox(const Text, Caption: PChar; Flags: Longint = MB_OK): Integer; begin Result := Application.MessageBox(PChar(Translate(string(Text))),PChar(Translate(string(Caption))),Flags); end; // JGB - Translate: use this to translate a string function TJvgLanguageLoader.Translate(const Text: string): string; begin // should be removed: spaces are always trimmed when using ini file if lofTrimSpaces in Options then Result := Trim(Text) else Result := Text; Result := FTranslationStrings.Values[Result]; if Result = '' then Result := Text; end; {$IFDEF USEJVCL} {$IFDEF UNITVERSIONING} initialization RegisterUnitVersion(HInstance, UnitVersioning); finalization UnregisterUnitVersion(HInstance); {$ENDIF UNITVERSIONING} {$ENDIF USEJVCL} end.