delphi3000.com Article

Multilanguage Easy Translator
Undertitle:MultilangTranslator localize your apps
URL:http://www.delphi3000.com/article.asp?ID=4594
Category:VCL-General
Uploader:Max Kleiner
 
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, "Für 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 instalación."
4002, "No se puede iniciar la instalación"
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.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 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 <= badDLLload then
    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 <= badDLLload then begin
    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 für Multilanguage Plattformen wie: W2K, XP, Win2003, etc.
    lid:= self.GetActualSystemLanguage;
    result:= currentSystemLanguage(lid)
  end
  else begin
    //Für 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 //Grösser 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.
Copyright 2000 delphi3000.com
Contact: delphi3000@bluestep.com'

Comments to this article
Write a new comment
Suggestion
    Alex Belyakov (Sep 12 2006 10:25AM)

Isn't it better for usability to have Localization strings in ini/xml files?
what about projects that use plugins?
Why not to check that some object has a property named 'Caption' or 'Hint'
Respond

RE: Suggestion
Max Kleiner (Sep 12 2006 3:03PM)

of course ini/xml files do have advantages but as Einstein said: as simple as possible but not simpler. A res file is easy to use (understandable) by everyone so external persons don't need a complicated tool.
An ini file with more informations has to convert in a resource file format. What you can do is to add comments to the res file:

STRINGTABLE
{
/* panMain: TPanel, Tag=1*/
4001, "Necesita derechos de administrador para la instalación."
/* btnInstal: TButton*/
4002, "No se puede iniciar la instalación"
/* btnClose: TButton*/
4003, "&Cerrar"
}
As you may see the tag is like the offset (addition rule)

A plugin is a project of your own so every project has its own resource linker. Switching the language without forms or tags in an addon goes like this:
with TMultilangSC.create(NIL) do begin
  LanguageOffset:= 4000; //e.g. Spain
  showmessage(getResourceString(3);
  Free;
end;
There are routines (filters) to check hints, caption, lines etc. in a dfm file and generate the res file.

  

Respond

RE: RE: Suggestion
Alex Belyakov (Sep 12 2006 3:16PM)

But when project is changed you must recompile it with those rc and transfer to customer each time.
I think that it's more useful to deploy language files seperately.
and when some resource string not found the user will see '' (emptyString)
because of
if LoadString(HInstance, number + fLanguage, pP, sizeof(pP))>0 then
    result:= pP
  else
    result:= '';
may be it's better to return defaultLanguage offset string here
Respond

RE: RE: RE: Suggestion
Max Kleiner (Sep 15 2006 3:14PM)

YES we can also load res at runtime:
library reslang;
uses
  SysUtils;
{$R 'filename.res'}
begin
end.

see under update1 in article, thanks for your hint;)
Respond

<%If Session("sSecurityLevel") >= 2 Then%>Additional Admin-Function:
Delete This Article-Comment! Delete this Comment!<%End If%>
Spanish StringTable
    Oscar Noe Martin (Sep 9 2006 3:36PM)

Great article & component, but I should correct your spanish strings.

Translated strings as follow (That are correct, but maybe not accurate at all)

STRINGTABLE
{
4001, "Necesita derechos de administrador para la instalación."
4002, "No se puede iniciar la instalación"
4003, "&Cerrar"
}


Respond

RE: Spanish StringTable
Max Kleiner (Sep 9 2006 10:51PM)

Thx for the flowers, changed it and put a pdf introduction too, bueno, casi perfecto ;)
Respond

<%If Session("sSecurityLevel") >= 2 Then%>Additional Admin-Function:
Delete This Article-Comment! Delete this Comment!<%End If%>