Mega Code Archive

 
Categories / Delphi / Examples
 

Multilanguage Easy Translator

Title: Multilanguage Easy Translator Question: For many projects you need to localize your application. An easy solution for all delphi platforms will be given no license or special tools needed. Just a small component and one straight resource file. Answer: I know there's a lot of solutions to localize, but this one... Let's start with the advantages: 1. Separation of strings from the form at design time 2. All languages linked in one executable at runtime (if necessary) 3. Lesser overhead for forms and easy to understand cause resources 4. Language change at runtime possible 5. No licence, component or additional tool needed 6. Runs on all Delphi versions, Lazarus and Kylix! 7. Whole translator engine in one unit (component) 8. Fast and easy to deploy 9. Easy change management of the Stringtable (just edit and compile) 10. Multiple use of one string to many controls possible 11. Extensible to any string or control 12. Event OnLanguagChanged() implemented 13. External Resource DLL at runtime 14. Switch between static linking or dynamic loading 15. Demo available for all delphi platforms (win32, CLX, dotnet, freepascal): for Delphi, dotnet & Kylix http://www.softwareschule.ch/download/delphi_multilang_demo.zip for freepascal/Lazarus http://www.softwareschule.ch/download/laz_multilang.zip The most obvious task in localizing an application is translating the strings that appear in the user interface from a form, a component or a database. To create an application that can be translated without altering code everywhere, the strings in the user interface should be isolated into a single module or file. In our case we put all the strings in one resource file called *.rc This resource file will be compiled with D:\Programme\Borland\Delphi7\Bin\brc32.exe -r filename.RC or straight forward in delphi project main form with the following compiler directive {$R 'filenameSTR.res' 'filenameSTR.RC'} so you don't need a resource batch or the resource compiler! (just in case of problems you get more error logs) At start- or at runtime we call objMultilang:= TMultiLangSC.Create(self); objMultilang.LanguageOffset:= 1000; //objMultilang.LanguageOffset:= objMultilang.currentLanguage; and all the well prepared strings (caption, hint, lines ...) will be translated. Note: strings from *.dfm are no longer visible, cause they now come from the linked resource file! Note: if no registry or ini will be defined you can set and call the language straight forward: case langRGroup1.itemindex of 0: objMultilang.LanguageOffset:= 0; 1: objMultilang.LanguageOffset:= 1000; 2: objMultilang.LanguageOffset:= 2000; 3: objMultilang.LanguageOffset:= 3000; 4: objMultilang.LanguageOffset:= 4000; end; Each language can have 999 strings {'D': Result:=0; 'E': Result:=1000; 'F': Result:=2000; 'I': Result:=3000; 'S': result:=4000;} Now a simple example of a resource file *.rc: (you can add this file in your delphi project) STRINGTABLE { 3, "Arbeiten im Team" 1003, "work in team" 2003, "travailler en groupe" 3003, "lavorare nel gruppo" 4003, "trabajo en equipo" } In this case we have to assign the value 3. In practice each language has its own section of STRINGTABLE. STRINGTABLE { 1, "Fr die Installation brauchen Sie Admininstratoren-Rechte." 2, "Setup kann nicht gestartet werden!" 3, "&Schliessen" } /**************************************************************************** ** English *****************************************************************************/ STRINGTABLE { 1001, "You require administrator rights for the installation." 1002, "Impossible to start setup!" 1003, "&Close" } /**************************************************************************** ** French *****************************************************************************/ STRINGTABLE { 2001, "Pour l'installation, vous avez besoin des droits d'administrateur." 2002, "Impossible de lancer le programme d'installation !" 2003, "&Fermer" } /**************************************************************************** ** Italian *****************************************************************************/ STRINGTABLE { 3001, "Per l'installazione sono necessari diritti d'amministratore." 3002, "L'installazione non pu essere avviata!" 3003, "&Chiudere" } /**************************************************************************** ** Spain *****************************************************************************/ STRINGTABLE { 4001, "Necesita derechos de administrador para la instalacin." 4002, "No se puede iniciar la instalacin" 4003, "&Cerrar" } Preparation of the form and the resource file: The magic behind is the tag property of a control. It stores an integer value as part of a component and has no predefined meaning. The Tag property is provided for the convenience of developers so in our case to define a relationship to the resource file! Changing the language of captions of controls on a form to a particular language means the tag property of the controls have to be set to values corresponding with the according resource strings. Leaving a Tag value 0 means that the caption of the according control isn't changed. Note that languages are distinguished by an offset of a multiple of 1000. For instance german is 0, English has an offset of 1000, French one of 2000 and so on. Important: each tag has a number between 0..999 : object bbtClose: TBitBtn, tag = 3 The component will add then the offset depending the current language! Currently the component (class TMultilangSC of the component MultilangTranslator) supports controls defined in : procedure ChangeComponent(theComponent: TComponent; const theLanguageOffset : integer); This concept can be easily extended to other controls not yet listed in the procedure ChangeComponent(). How it works: ------------------------------------------------------------------- Delphi automatically creates a .dfm (.xfm in CLX applications) file that contains the resources for your menus, dialogs, and bitmaps (Streaming and filing of resources are inherited from TPersistent). After a component reads all its property values from its stored description, it calls a virtual method named Loaded, which performs any required initializations. The call to Loaded occurs before the form and its controls are shown, so you don't need to worry about initialization causing flicker on the screen. The sequence is the following: function TMultiLangSC.currentLanguage: integer; property LanguageOffset: integer read fLanguage write SetLanguage; SetLanguage(const Value: integer); procedure TMultilangSC.ChangeLanguage(const languageOffset: integer); ChangeComponent(GetTopComponent,languageOffset); if Assigned(fOnLanguageChanged) then fOnLanguageChanged(Self); Here's an extract of the important method ChangeComponent: begin if theComponent.ComponentCount0 then begin for x:= 0 to theComponent.ComponentCount-1 do ChangeComponent(theComponent.Components[x],theLanguageOffset); end; if theComponent.tag 0 then begin if (theComponent is TForm) then (theComponent as TForm).Caption:= GetResourceString(theComponent.tag) else if (theComponent is TLabel) then (theComponent as TLabel).Caption:= GetResourceString(theComponent.tag) else if (theComponent is TImage) then (theComponent as TImage).Hint:= GetResourceString(theComponent.tag) ..... To initialize a component after it loads its property values, override the Loaded method. This is done when you use the translator component from the component palette. In addition to these obvious user interface elements, you will need to isolate any strings, such as error messages or string literals, that you present to the user. String resources are not included in the form file. You can isolate them by declaring constants for them using the resourcestring keyword. In our case we put them also in the resource file! Therefore its possible to use the function GetResourceString at any time to load MultilangTranslator strings from the language resource file. This is especially necessary if the Translator should be used independently from a TForm or a visual component. This is how we call the API function: showmessage(objMultilang.GetResourceString(21)); or in a memo component without tags: memInfo.lines.Add(objMultilang.GetResourceString(11)); memInfo.lines.Add(''); memInfo.lines.Add(objMultilang.GetResourceString(12)); Isolating resources simplifies the translation process. The next level of resource separation is the creation of a resource DLL. A resource DLL contains all the resources and only the resources for a program. Resource DLLs allow you to create a program that supports many translations simply by swapping the resource DLL. Note: The Translation Manager in Delphi provides a mechanism for viewing and editing translated resources. To open the Translation Manager from within the IDE, choose View|Translation Manager. Before you can use the Translation Manager in the IDE, you must add languages to your project using the Resource DLL wizard. You will get n directories and n files with a big overhead and complicated rules! ------------------------------------------------------------- Update 1: If we want to change the resfile at runtime (means no compilation) we have to build a resfile DLL in a new project: library reslang; uses SysUtils; {$R 'filename.res'} begin end. Second we have to load the DLL and adjust the HInstance call in our component (function getResourceString()), here's the proof of concept (so we can get all resources from a runtime dll!): procedure TForm1.FormCreate(Sender: TObject); const badDLLload = 1; var h: tHandle; pP: array[0..255] of char; begin h:= loadLibrary('reslang.dll'); if h showmessage('no dll load') else begin if loadString(h, 5, pP, sizeof(pP)) 0 then showmessage((pP)); //... end; end; Update 2, Version 1.4: A port for CLX has to be done, small changes with a sound interface: function TMultilangSC.GetResourceString(Ident: Integer): string; var StrData: TStrData; begin StrData.Ident:= Ident + LanguageOffset; StrData.Str:= ''; EnumResourceModules(EnumStringModules, @StrData); Result:= StrData.Str; end; ---------------------------------------------------------- There's an introduction show on http://max.kleiner.com/download/multilang_intro.pdf (630kb) or a technical report on german at: http://www.softwareschule.ch/download/pascal_multilanguage.pdf (124kb) Update always on: http://www.softwareschule.ch/download/laz_multilang.zip ***************************************************************************************** ----------------------------------------------------------------------------------------- unit MultilangTranslator; (* Author: kleiner kommunikation Kleiner, armasuisse max@kleiner.com Date: Mai 2003 juni 2005, resources, pascal analyzer, spain extension juli 2006, Max Kleiner Framework FIS-HE aug 2006 set Spain, more comps., resolve update problem sep 2006 dynamic change in same form on instance Jan 2007 samll changes for Lazarus 0.9 Sep 2007 extended with dll loading locs= 390, 10.09.2007 Description: Changing the language of strings(caption, hint, lines ...) of controls on a form to a particular language. The Tag property of the controls has to be set to values corresponding with the according resource strings. Leaving a Tag value 0 means that the caption of the according control isn't changed. Note that languages are distinguished by an offset of a multiple of 1000. For instance german is 0, English has an offset of 1000, French has one of 2000 and Italian's is 3000. Extract of a resource file *.rc: STRINGTABLE { 3, "Arbeiten im Team" 1003, "work in team1" 2003, "travailler en groupe" 3003, "lavorare nel gruppo" 4003, "trabajo en equipo" } In this case to a Tag of a control which should show this text in the proper language we have to assign the value 3. Can be done with the ObjInspector. Currently TMultilangSC supports the controls in : procedure ChangeComponent(theComponent: TComponent; const theLanguageOffset : integer); This concept can be easily extended to other controls not listed in procedure ChangeComponent. Languages string are assigned to the particular captions after a forms has been loaded from the resources. However its possible to use the function GetResourceString at any time to load language dependend strings from the language resource file. This is especially necessary if TMultiLangSC is used independendly from a TForm. Caller example: objMultilang:= TMultiLangSC.Create(self); objMultilang.ResDLL:= 'reslang2.dll'; // in case of resDLL objMultilang.LanguageOffset:= 2000; // in case of French Version: 1.5, Implementation with Component linking or by runtime *) interface uses Windows, Messages, SysUtils, Classes; type tLangChanging = procedure(Sender: TObject; theComponent: TComponent) of object; tLangChanged = procedure(Sender: TObject) of object; TMultilangSC = class(TComponent) private fLanguage: integer; fResDLL: string; fResDLLHandle: tHandle; sDLLState: boolean; fOnLangChanging: tLangChanging; fOnLangChanged: tLangChanged; procedure SetLanguage(const Value: integer); procedure ChangeLanguage(const languageOffset: integer); procedure ChangeComponent(theComponent: TComponent; const theLanguageOffset : integer); function GetTopComponent: TComponent; function IsOSMultilanguage: boolean; function GetActualSystemLanguage: word; protected //Loaded Initializes the component after the form file has been //read into memory. procedure Loaded; override; procedure setResDLL(sDLLPath: ansiString); public constructor Create(AOwner: TComponent); override; function GetResourceString(const number: integer): string; function currentLanguage: integer; function currentSystemLanguage(mylid:word): integer; function currentUserLanguage: integer; property LanguageOffset: integer read fLanguage write SetLanguage; property ResDLL: string read fResDLL write setResDLL; published {change of published names since version 1.5} property OnLangChanging: tLangChanging read fOnLangChanging write fOnLangChanging; property OnLangChanged: tLangChanged read fOnLangChanged write fOnLangChanged; end; procedure Register; //var objMultilang: TMultilangSC; implementation Uses Registry, Forms, StdCtrls, ComCtrls, ExtCtrls, Menus, Buttons; // START resource string wizard section resourcestring SSecLangDep_Kernel32Dll = 'kernel32.dll'; SSecLangDep_Language = 'Language'; SSecLangDep_SecureCenterXP = 'SecureCenterXP'; SSecLangDep_SOFTWAREGSTSecureCenterXP = '\SOFTWARE\GST\SecureCenterXP'; // END resource string wizard section procedure Register; begin RegisterComponents(SSecLangDep_SecureCenterXP, [TMultilangSC]); end; { TMultiLangSC} constructor TMultiLangSC.Create(AOwner: TComponent); // this creates an instance of TSecureLanguageDepenend and initializes // its member variables. begin inherited; fLanguage:= 0; fOnLangChanging:= NIL; fOnLangChanged:= NIL; // static linking resources sDLLState:= false; end; function TMultiLangSC.currentLanguage: integer; // this function reads from the registry the current language // use for SecureCenterXP. It returns the base index to the // the according language strings. A particular string then // can be accessed adding its offset to this base value. var rReg: TRegistry; languageStr: string; begin rReg:= TRegistry.Create; languageStr:= '?'; try with rReg do begin try RootKey:= HKEY_LOCAL_MACHINE; OpenKeyReadOnly(SSecLangDep_SOFTWAREGSTSecureCenterXP); languageStr:= ReadString(SSecLangDep_Language); except end; end; finally rReg.Free; end; if languageStr '' then begin case languageStr[1] of 'D': Result:=0; 'E': Result:=1000; 'F': Result:=2000; 'I': Result:=3000; 'S': result:=4000; else Result:= 0; end; end else Result:= 0; end; procedure TMultilangSC.SetLanguage(const Value: integer); // changes the language of a component tree (usually a form) begin fLanguage:= Value; if not (csLoading in ComponentState) then ChangeLanguage(fLanguage); end; procedure TMultilangSC.setResDLL(sDLLPath: ansiString); const badDLLload = 1; begin fResDLL:= sDLLPath; //private handle fResDLLHandle:= loadLibrary(pchar(sDLLPath)); if fResDLLHandle messageBox(0,'no langauage_dll loaded','Multilang DLL',MB_ICONERROR); sDLLState:= false; end else sDLLState:= true; end; function TMultilangSC.GetResourceString(const number : integer) : string; // reads a string from the resource file. As a parameter this function takes the // offset of the string relative to the base index fLanguage. // compile with {-Sd} var pP: array[0..255] of char; begin //state event if sDLLState then HInstance:= fResDLLHandle; if LoadString(HInstance, number + fLanguage, pP, sizeof(pP))0 then result:= pP else result:= ''; end; function TMultilangSC.GetTopComponent: TComponent; // searches upwards through a tree of components until its root is found // or a component is of type TForm. var x: TComponent; begin x:= Self; Result:= x; // prevent compiler warning while (Assigned(x)) and not (x is TForm) do begin Result:= x; x:= x.Owner; end; if Assigned(x) then Result:= x; end; procedure TMultilangSC.ChangeLanguage(const languageOffset: integer); // this method changes the language of a component tree, if we are not in design mode. // after the whole tree of components has been change the event fOnLanguageChanged is // called, if a value has been assigned to it. This give the client the opportunity // to do his own language specific text assignments using GetResourceString. begin if not (csDesigning in ComponentState) then begin ChangeComponent(GetTopComponent,languageOffset); if Assigned(fOnLangChanged) then fOnLangChanged(Self); end; end; procedure TMultilangSC.ChangeComponent(theComponent: TComponent; const theLanguageOffset: integer); // this function changes the language of the components text fields recursively. // for every component an event fOnLanguageChanging is called if a handler was // assigned to it. This gives the client the opportunity to do additional language // specific treatments on a component level. If for instance a component is a grid, // the client can use this event to test whether this grid is the current processed // component and if true he could use the opportunity to change column or // row names using GetResourceString var x : integer; begin if theComponent.ComponentCount 0 then begin for x:= 0 to theComponent.ComponentCount-1 do ChangeComponent(theComponent.Components[x], theLanguageOffset); end; if theComponent.tag 0 then begin if (theComponent is TForm) then (theComponent as TForm).Caption:= GetResourceString(theComponent.tag) else if (theComponent is TLabel) then (theComponent as TLabel).Caption:= GetResourceString(theComponent.tag) else if (theComponent is TCheckBox) then (theComponent as TCheckBox).Caption:= GetResourceString(theComponent.tag) else if (theComponent is TToolButton) then (theComponent as TToolButton).Hint:= GetResourceString(theComponent.tag) else if (theComponent is TButton) then (theComponent as TButton).Caption:= GetResourceString(theComponent.tag) else if (theComponent is TRadioButton) then (theComponent as TRadioButton).Caption:= GetResourceString(theComponent.tag) else if (theComponent is TGroupBox) then (theComponent as TGroupBox).Caption:= GetResourceString(theComponent.tag) else if (theComponent is TPanel) then (theComponent as TPanel).Caption:= GetResourceString(theComponent.tag) else if (theComponent is TTabSheet) then (theComponent as TTabSheet).Caption:= GetResourceString(theComponent.tag) else if (theComponent is TMenuItem) then (theComponent as TMenuItem).Caption:= GetResourceString(theComponent.tag) else if (theComponent is TImage) then (theComponent as TImage).Hint:= GetResourceString(theComponent.tag) else if (theComponent is TRadioGroup) then (theComponent as TRadioGroup).caption:= GetResourceString(theComponent.tag); if Assigned(fOnLangChanging) then fOnLangChanging(Self, theComponent); end; end; procedure TMultilangSC.Loaded; begin inherited; LanguageOffset:= currentLanguage; end; function TMultilangSC.currentSystemLanguage(mylid: word): integer; begin case mylid of // german dialects $0407, {German (Standard)} $0807, {German (Switzerland)} $0c07, {German (Austria)} $1007, {German (Luxembourg)} $1407: {German (Liechtenstein)} Result := 0; // french dialects $040c, { French (Standard)} $080c, { French (Belgian)} $0c0c, { French (Canadian)} $100c, { French (Switzerland)} $140c, { French (Luxembourg)} $180c: { Windows 98/Me, Windows 2000/XP: French (Monaco)} Result := 2000; // english dialects $0409, { English (United States)} $0809, { English (United Kingdom)} $0c09, { English (Australian)} $1009, { English (Canadian)} $1409, { English (New Zealand)} $1809, { English (Ireland)} $1c09, { English (South Africa)} $2009, { English (Jamaica)} $2409, { English (Caribbean)} $2809, { English (Belize)} $2c09, { English (Trinidad)} $3009, { Windows 98/Me, Windows 2000/XP: English (Zimbabwe)} $3409: { Windows 98/Me, Windows 2000/XP: English (Philippines)} Result := 1000; $0410, { Italian (Standard)} $0810: { Italian (Switzerland)} Result := 3000; //LANG_SPANISH = $0a; //{$EXTERNALSYM LANG_SPANISH} //$01; { Spanish (Castilian) $040a: result:= 4000; else Result:= 0; end; end; function TMultilangSC.currentUserLanguage: integer; var lid: word; begin if self.IsOSMultilanguage then begin //Nur fr Multilanguage Plattformen wie: W2K, XP, Win2003, etc. lid:= self.GetActualSystemLanguage; result:= currentSystemLanguage(lid) end else begin //Fr alle andern Plattfomen wie: Win95, Win98, ME, NT lid:= GetSystemDefaultLangID; result:= currentSystemLanguage(lid); end; end; function TMultilangSC.IsOSMultilanguage: boolean; var aOsInfo: TOSVersionInfo; begin aOsInfo.dwOSVersionInfoSize:= SizeOf(TOSVersionInfo); GetVersionEx(aOsInfo); if aOsInfo.dwMajorVersion = 5 then //Grsser als 5 ist W2K oder XP oder 2003 result:= true else result:= false; end; //function GetUserDefaultUILanguage:word; stdcall; external 'kernel32.dll'; function TMultilangSC.GetActualSystemLanguage: Word; type FunctionWithDWORDReturnValue = function: DWORD; stdcall; var libInstance: HINST; GetUserDefaultUILanguage: FunctionWithDWORDReturnValue; begin //result := GetUserDefaultUILanguage; result:= 0; libInstance:= LoadLibrary('kernel32.dll'); try if libInstance 0 then begin GetUserDefaultUILanguage:= GetProcAddress(libInstance, 'GetUserDefaultUILanguage'); Result:= GetUserDefaultUILanguage; end; finally FreeLibrary(libInstance); end; end; initialization //objMultilang:= TMultiLangSC.Create(NIL); finalization //objMultilang.Free; end.