{-----------------------------------------------------------------------------
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: JvTranslator.PAS, released on 2002-06-03

The Initial Developer of the Original Code is Sébastien Buysse [sbuysse att buypin dott com]
Portions created by Sébastien Buysse are Copyright (C) 2001 Sébastien Buysse.
All Rights Reserved.

Contributor(s): _________________________________.

You may retrieve the latest version of this file at the Project JEDI's JVCL home page,
located at http://jvcl.delphi-jedi.org

Known Issues:
-----------------------------------------------------------------------------}
// $Id$

unit JvTranslator;

{$I jvcl.inc}

interface

uses
  {$IFDEF UNITVERSIONING}
  JclUnitVersioning,
  {$ENDIF UNITVERSIONING}
  SysUtils, Classes, IniFiles,
  Forms, ComCtrls, Menus, Dialogs,
  JvSimpleXml, JvComponentBase,
  JclSimpleXml;

type
  {$IFDEF RTL230_UP}
  [ComponentPlatformsAttribute(pidWin32 or pidWin64 or pidOSX32)]
  {$ENDIF RTL230_UP}
  TJvTranslator = class(TJvComponent)
  private
    FXML: TJvSimpleXml;
    FSkipList: TList;
    function IsObject(const Obj: TClass; const ClassName: string): Boolean;
  protected
    function FindItemNamed(Root: TJvSimpleXMLElem; const AName: string;
      ARecurse: Boolean = False): TJvSimpleXMLElem; virtual;
    procedure TranslateComponent(const Component: TComponent; const Elem: TJvSimpleXMLElem); virtual;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    // Call SkipClass to register a class to skip when reading/writing
    procedure SkipClass(AClass: TClass);
    // Call UnskipClass to unregister a class so it won't be skip when reading/writing
    procedure UnskipClass(AClass: TClass);
    // Call SkipProperty to register a class property to skip when reading/writing
    // If UnskipClass has already been called for this class, does nothing
    procedure SkipProperty(AClass: TClass; const PropName: string);
    // Call UnskipProperty to unregister a class property so it won't be skipped when reading/writing
    // If SkipClass has already been called for this class, does nothing
    procedure UnskipProperty(AClass: TClass; const PropName: string);
    // Returns True if the specifed class/object/property is in the skip list
    function InSkipList(AClass: TClass): Boolean; overload;
    function InSkipList(Obj: TObject): Boolean; overload;
    function InSkipList(AClass: TClass; const PropName: string): Boolean; overload;
    function InSkipList(Obj: TObject; const PropName: string): Boolean; overload;
    procedure ClearSkipList;
    // ComponentToXML converts a TComponent and, optionally, it's owned components to an XML string
    // and returns it
    function ComponentToXML(const AComponent: TComponent; Recurse: Boolean): string;
    // Translate the entire Application using the file Filename
    procedure Translate(const FileName: string); overload;
    // Translate the entire Application using a stream
    procedure Translate(const Stream: TStream); overload;
    // Translate the entire Application using a string
    procedure TranslateString(const S: string); overload;
    // Translate a form using the file Filename
    procedure Translate(const FileName: string; const Form: TCustomForm); overload;
    // Translate a form using a stream
    procedure Translate(const Stream: TStream; const Form: TCustomForm); overload;
    // Translate a form using a string
    procedure TranslateString(const S: string; const Form: TCustomForm); overload;
    // Translate a form using the currently loaded XML (wherever it came from)
    procedure Translate(const Form: TCustomForm); overload;
    // Translates all form instances owned by the global screen object using the file Filename
    procedure TranslateScreen(const FileName: string); overload;
    // Translates all form instances owned by the global screen object using a stream
    procedure TranslateScreen(const Stream: TStream); overload;
    // Translates all form instances owned by the global screen object using a string
    procedure TranslateScreenString(const S: string);
    // Returns the value of a node or a property value of a node based on certain search criteria.
    // To find the value, the method first searches the root for a subnode with the name in Category.
    // If found, Category is searched for a subnode with the name in Item. If found, either the value
    // of Item or the value of a property named "Value" in Item is returned.
    // Structurally it should look something like this:
    // <Root>
    //   <Category>
    //     <Item Value="PropValue">Value</Item>
    //   </Category>
    //   ....
    // This method returns either Value or, if not found, PropValue or, if not found, an empty string
    function Translate(const Category, Item: string): string; overload;
    property XML: TJvSimpleXml read FXML;
  end;

  {$IFDEF RTL230_UP}
  [ComponentPlatformsAttribute(pidWin32 or pidWin64 or pidOSX32)]
  {$ENDIF RTL230_UP}
  TJvTranslatorStrings = class(TJvComponent)
  private
    FList: THashedStringList;
    function GetString(Index: Integer): string;
    procedure SetString(Index: Integer; const Value: string);
    function GetCount: Integer;
    function GetValue(Index: Integer): string;
  public
    constructor Create(AOwner: TComponent); override;
    destructor Destroy; override;
    function IndexOf(const Name: string): Integer;
    function Add(const Name: string; var Value: string): Integer;
    // (p3) this is weird: GetString returns the *Name* but SetString sets the *Value*...
    property Strings[Index: Integer]: string read GetString write SetString; default;
    property Value[Index: Integer]: string read GetValue;
    property Count: Integer read GetCount;
  end;

{$IFDEF UNITVERSIONING}
const
  UnitVersioning: TUnitVersionInfo = (
    RCSfile: '$URL$';
    Revision: '$Revision$';
    Date: '$Date$';
    LogPath: 'JVCL\run'
  );
{$ENDIF UNITVERSIONING}

implementation

uses
  TypInfo,
  {$IFDEF SUPPORTS_INLINE}
  Windows,
  {$ENDIF SUPPORTS_INLINE}
  JclStreams,
  JvConsts;

const
  cName = 'Name';
  cItem = 'Item';
  cIndex = 'Index';
  cColumn = 'Column';
  cValue = 'Value';
  cVariables = 'Variables';
  cTTreeNodes = 'TTreeNodes';
  cTListItems = 'TListItems';
  cTStrings = 'TStrings';
  cTCollection = 'TCollection';
  cTComponent = 'TComponent';
  cTJvTranslatorStrings = 'TJvTranslatorStrings';
  cNewline = '\n';

type
  PSkipPropRec = ^TSkipPropRec;
  TSkipPropRec = record
    AClass: TClass;
    AProps: TStringList;
  end;

function InternalGetWideStrProp(Instance: TObject; const PropName: string): WideString; overload;
begin
  Result := {$IFDEF RTL240_UP}GetStrProp{$ELSE}GetWideStrProp{$ENDIF RTL240_UP}(Instance, PropName);
end;

function InternalGetPropList(AObject: TObject; out PropList: PPropList): Integer;
begin
  Result := GetPropList(AObject, PropList);
end;

//=== { TJvTranslator } ======================================================

constructor TJvTranslator.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FXML := TJvSimpleXml.Create(nil);
  SkipProperty(TComponent, cName);
end;

destructor TJvTranslator.Destroy;
begin
  FXML.Free;
  ClearSkipList;
  FreeAndNil(FSkipList);
  inherited Destroy;
end;

function TJvTranslator.FindItemNamed(Root: TJvSimpleXMLElem; const AName: string; ARecurse: Boolean): TJvSimpleXMLElem;
var
  I: Integer;
begin
  Result := nil;
  if Root = nil then
    Root := FXML.Root;
  if AnsiSameText(Root.Name, AName) then
    Result := Root
  else
  if not ARecurse then
    Result := Root.Items.ItemNamed[AName]
  else
    for I := 0 to Root.Items.Count - 1 do
    begin
      Result := FindItemNamed(Root.Items[I], AName, True);
      if Result <> nil then
        Break;
    end;
end;

function TJvTranslator.IsObject(const Obj: TClass; const ClassName: string): Boolean;
begin
  if Obj = nil then
    Result := False
  else
    Result := SameText(Obj.ClassName, ClassName) or (IsObject(Obj.ClassParent, ClassName));
end;

function TJvTranslator.ComponentToXML(const AComponent: TComponent; Recurse: Boolean): string;
var
  AName: string;
  AElem: TJvSimpleXMLElem;

  procedure CollectionToXML(Collection: TCollection; Elem: TJvSimpleXMLElem; Recurse:Boolean); forward;

  procedure TreeNodesToXML(Nodes: TTreeNodes; Elem: TJvSimpleXMLElem);
  var
    N: TTreeNode;
    AElem: TJvSimpleXMLElem;
  begin
    // format: <Items>
    //           <Item Index="" Value="" />
    // TODO
    if InSkipList(Nodes) then
      Exit;
    N := Nodes.GetFirstNode;
    while Assigned(N) do
    begin
      if not InSkipList(N) then
      begin
        AElem := Elem.Items.Add(cItem);
        AElem.Properties.Add(cIndex, N.Index);
        AElem.Properties.Add(cValue, N.Text);
      end;
      {
            AElem.Properties.Add('ImageIndex',N.ImageIndex);
            AElem.Properties.Add('SelectedIndex',N.SelectedIndex);
      }
      N := N.GetNext;
    end;
  end;

  procedure ListItemsToXML(Items: TListItems; Elem: TJvSimpleXMLElem);
  var
    I, J: Integer;
    AElem: TJvSimpleXMLElem;
  begin
    // format: <Items>
    //           <Item Index="" Column="" Value="" />
    // TODO
    if InSkipList(Items) then
      Exit;
    for I := 0 to Items.Count - 1 do
    begin
      if not InSkipList(Items[I]) then
      begin
        AElem := Elem.Items.Add(cItem);
        AElem.Properties.Add(cIndex, I);
        AElem.Properties.Add(cColumn, 0);
        AElem.Properties.Add(cValue, Items[I].Caption);
        for J := 0 to Items[I].SubItems.Count - 1 do
        begin
          AElem := Elem.Items.Add(cItem);
          AElem.Properties.Add(cIndex, I);
          AElem.Properties.Add(cColumn, J + 1);
          AElem.Properties.Add(cValue, Items[I].SubItems[J]);
        end;
      end;
    end;
  end;

  procedure StringsToXML(Strings: TStrings; Elem: TJvSimpleXMLElem);
  var
    I: Integer;
    AElem: TJvSimpleXMLElem;
  begin
    // format: <Items>
    //           <Item Index="" Value="" />
    if InSkipList(Strings) then
      Exit;
    for I := 0 to Strings.Count - 1 do
    begin
      AElem := Elem.Items.Add(cItem);
      AElem.Properties.Add(cIndex, I);
      AElem.Properties.Add(cValue, Strings[I]);
    end;
  end;

  procedure TranslatorStringsToXML(AStrings: TJvTranslatorStrings; Elem: TJvSimpleXMLElem);
  var
    I: Integer;
    AElem: TJvSimpleXMLElem;
  begin
    // I'm not sure how to create a translation template for this component, so this is just a guess...
    // format:
    // <Variables>
    //   <Item Name="" Value="" />
    // </Variables>
    if InSkipList(AStrings) then
      Exit;
    Elem.Name := cVariables;
    for I := 0 to AStrings.Count - 1 do
    begin
      AElem := Elem.Items.Add(cItem);
      AElem.Properties.Add(cName, AStrings[I]);
      AElem.Properties.Add(cValue, AStrings.Value[I]);
    end;
  end;
(*
  procedure ObjectToXML(AnObject: TObject; Elem: TJvSimpleXMLElem);
  var
    J, Count: Integer;
    PropList: PPropList;
    PropName: string;
    PropInfo: PPropInfo;
    AnObj: TObject;
  begin
    if (AnObject <> nil) and not InSkipList(AnObject) then
    begin
      Count := InternalGetPropList(AnObject, PropList);
      for J := 0 to Count - 1 do
      begin
        PropInfo := PropList[J];
        PropName := PropInfo^.Name;
        try
          if (PropInfo^.SetProc = nil) or InSkipList(AnObject, PropName) then
            Continue;
          case PropInfo^.PropType^.Kind of
            tkInteger:
              Elem.Properties.Add(PropName, GetOrdProp(AnObject, PropName));
            tkEnumeration:
              Elem.Properties.Add(PropName, GetEnumProp(AnObject, PropName));
            tkSet:
              Elem.Properties.Add(PropName, GetSetProp(AnObject, PropName));
            {$IFDEF UNICODE} tkUString, {$ENDIF}
            tkString, tkLString:
              Elem.Properties.Add(PropName, GetStrProp(AnObject, PropName));
            tkClass:
              begin
                AnObj := GetObjectProp(AnObject, PropName);
                if IsObject(AnObj.ClassType, cTTreeNodes) then
                  TreeNodesToXML(TTreeNodes(AnObj), Elem.Items.Add(PropName))
                else
                if IsObject(AnObj.ClassType, cTListItems) then
                  ListItemsToXML(TListItems(AnObj), Elem.Items.Add(PropName))
                else
                if IsObject(AnObj.ClassType, cTStrings) then
                  StringsToXML(TStrings(AnObj), Elem.Items.Add(PropName))
                else
                if IsObject(AnObj.ClassType, cTCollection) then
                  CollectionToXML(TCollection(AnObj), Elem.Items.Add(PropName))
                else
                if not IsObject(AnObj.ClassType, cTComponent) then
                  // NB! TComponents are excluded because most of the time, a published TComponent
                  // property references another component on the form. In some cases, however, a TComponent
                  // *can* be an internal component and this code won't list it.
                  // No known solution yet (no, HasParent/GetParentComponent doesn't work here)
                  ObjectToXML(AnObj, Elem.Items.Add(PropName));
              end;
          end;
        except
          //
        end;
      end;
    end;
  end;
*)
  procedure InnerComponentToXML(AComponent: TObject; Elem: TJvSimpleXMLElem; Recurse: Boolean);
  var
    I, Count: Integer;
    PropList: PPropList;
    PropName: string;
    PropInfo: PPropInfo;
    AnObj: TObject;
  begin
    if AComponent = nil then
      Exit;
    if not InSkipList(AComponent) then
    begin
      if IsObject(AComponent.ClassType, cTJvTranslatorStrings) then
      begin
        TranslatorStringsToXML(TJvTranslatorStrings(AComponent), Elem);
        Exit;
      end;
      Count := InternalGetPropList(AComponent, PropList);
      for I := 0 to Count - 1 do
      begin
        PropInfo := PropList[I];
        PropName := {$IFDEF SUPPORTS_UNICODE}UTF8ToString{$ENDIF SUPPORTS_UNICODE}(PropInfo^.Name);

        if InSkipList(AComponent, PropName) or (PropInfo^.SetProc = nil) then
          Continue;
        case PropInfo^.PropType^.Kind of
          tkInteger:
            Elem.Properties.Add(PropName, GetOrdProp(AComponent, PropName));
          tkEnumeration:
            Elem.Properties.Add(PropName, GetEnumProp(AComponent, PropName));
          tkSet:
            Elem.Properties.Add(PropName, GetSetProp(AComponent, PropName));
          {$IFDEF UNICODE} tkUString, {$ENDIF}
          tkString, tkLString, tkWString:
            Elem.Properties.Add(PropName, XMLEncode(InternalGetWideStrProp(AComponent, PropName)));
          tkClass:
            begin
              AnObj := GetObjectProp(AComponent, PropName);

              // The property may not be assigned (action, popupmenu...), in
              // this case, we can't do anything with it.
              if not Assigned(AnObj) then
                Continue;

              if IsObject(AnObj.ClassType, cTTreeNodes) then
                TreeNodesToXML(TTreeNodes(AnObj), Elem.Items.Add(PropName))
              else
              if IsObject(AnObj.ClassType, cTListItems) then
                ListItemsToXML(TListItems(AnObj), Elem.Items.Add(PropName))
              else
              if IsObject(AnObj.ClassType, cTStrings) then
                StringsToXML(TStrings(AnObj), Elem.Items.Add(PropName))
              else
              if IsObject(AnObj.ClassType, cTCollection) then
                CollectionToXML(TCollection(AnObj), Elem.Items.Add(PropName), Recurse)
              else
              if not IsObject(AnObj.ClassType, cTComponent) then
                // NB! TComponents are excluded because most of the time, a published TComponent
                // property references another component on the form. In some cases, however, a TComponent
                // *can* be an internal component and this code won't list it.
                // No known solution yet (no, HasParent/GetparentComponent doesn't work here)
                InnerComponentToXML(AnObj, Elem.Items.Add(PropName), Recurse);
            end;
        end;
      end;
    end;
    if Recurse and (AComponent is TComponent) then
      for I := 0 to TComponent(AComponent).ComponentCount - 1 do
        if TComponent(AComponent).Components[I].Name <> '' then
          InnerComponentToXML(TComponent(AComponent).Components[I], Elem.Items.Add(TComponent(AComponent).Components[I].Name), True);
  end;
  procedure CollectionToXML(Collection: TCollection; Elem: TJvSimpleXMLElem; Recurse:Boolean);
  var
    I: Integer;
  begin
    if not InSkipList(Collection) then
      for I := 0 to Collection.Count - 1 do
        if not InSkipList(Collection.Items[I]) then
          InnerComponentToXML(Collection.Items[I], Elem.Items.Add(Collection.Items[I].DisplayName), Recurse);
  end;

begin
  Result := '';
  FXML.Root.Clear;
  if AComponent = nil then
    Exit;
  if AComponent is TApplication then
  begin
    AName := TApplication(AComponent).Title;
    FXML.Root.Name := 'Translation'; // DO NOT LOCALIZE
    AElem := FXML.Root.Items.Add(AName);
  end
  else
  begin
    AName := TComponent(AComponent).Name;
    AElem := FXML.Root;
    FXML.Root.Name := AName;
  end;
  if AName <> '' then
  begin
    InnerComponentToXML(AComponent, AElem, Recurse);
    Result := FXML.Root.SaveToString;
  end;
end;

procedure TJvTranslator.Translate(const FileName: string);
begin
  try
    FXML.LoadFromFile(FileName);
    TranslateComponent(Application, FXML.Root);
  except
  end;
end;

procedure TJvTranslator.Translate(const Stream: TStream);
begin
  try
    FXML.LoadFromStream(Stream);
    TranslateComponent(Application, FXML.Root);
  except
  end;
end;

procedure TJvTranslator.TranslateScreen(const FileName: string);
var
  I: Integer;
begin
  try
    FXML.LoadFromFile(FileName);
    for I := 0 to Screen.FormCount - 1 do
      Translate(Screen.Forms[I]);
  except
  end;
end;

procedure TJvTranslator.TranslateScreen(const Stream: TStream);
var
  I: Integer;
begin
  try
    FXML.LoadFromStream(Stream);
    for I := 0 to Screen.FormCount - 1 do
      Translate(Screen.Forms[I]);
  except
  end;
end;

procedure TJvTranslator.Translate(const FileName: string; const Form: TCustomForm);
begin
  try
    FXML.LoadFromFile(FileName);
    Translate(Form);
  except
  end;
end;

procedure TJvTranslator.TranslateComponent(const Component: TComponent;
  const Elem: TJvSimpleXMLElem);
var
  I, J: Integer;
  PropInfo: PPropInfo;
  Obj: TObject;
  Ok: Boolean;
  S: string;

  procedure TransObject(const Obj: TObject; const Elem: TJvSimpleXMLElem); forward;

  function AnalyseCRLF(Value: string): string;
  begin
    Result := StringReplace(Value, cNewline, sLineBreak, [rfReplaceAll]);
  end;

  procedure TransStrings(const Obj: TObject; const Elem: TJvSimpleXMLElem);
  var
    I, J: Integer;
  begin
    if (Elem.Items.Count > 0) and (Elem.Items[0] is TJvSimpleXmlElemCData) then
      TStrings(Obj).Text := Elem.Items[0].Value
    else
      for I := 0 to Elem.Items.Count - 1 do
      begin
        J := Elem.Items[I].Properties.IntValue(cIndex, MaxInt);
        if J < TStrings(Obj).Count then
          TStrings(Obj).Strings[J] := Elem.Items[I].Properties.Value(cValue);
      end;
  end;

  procedure TransTreeNodes(const Obj: TObject; const Elem: TJvSimpleXMLElem);
  var
    I, J: Integer;
  begin
    for I := 0 to Elem.Items.Count - 1 do
    begin
      J := Elem.Items[I].Properties.IntValue(cIndex, MaxInt);
      if J < TTreeNodes(Obj).Count then
        TTreeNodes(Obj).Item[J].Text := Elem.Items[I].Properties.Value(cValue);
    end;
  end;

  procedure TransVars;
  var
    I, J: Integer;
  begin
    with TJvTranslatorStrings(Component) do
      for I := 0 to Elem.Items.Count - 1 do
      begin
        J := TJvTranslatorStrings(Component).IndexOf(Elem.Items[I].Properties.Value(cName));
        if J <> -1 then
          TJvTranslatorStrings(Component).Strings[J] := AnalyseCRLF(Elem.Items[I].Properties.Value(cValue));
      end;
  end;

  procedure TransListItems(const Obj: TObject; const Elem: TJvSimpleXMLElem);
  var
    I, J: Integer;
  begin
    for I := 0 to Elem.Items.Count - 1 do
    begin
      J := Elem.Items[I].Properties.IntValue(cIndex, MaxInt);
      if J < TListItems(Obj).Count then
        with TListItems(Obj).Item[J] do
        begin
          J := Elem.Items[I].Properties.IntValue(cColumn, MaxInt);
          if J = 0 then
            Caption := Elem.Items[I].Properties.Value(cValue)
          else
          begin
            Dec(J);
            if J < SubItems.Count then
              SubItems[J] := Elem.Items[I].Properties.Value(cValue);
          end;
        end;
    end;
  end;

  procedure TransProperties(const Obj: TObject; const Elem: TJvSimpleXMLElem);
  var
    I, J: Integer;
    PropInfo: PPropInfo;
    S: string;
  begin
    if Obj = nil then
      Exit;
    for I := 0 to Elem.Properties.Count - 1 do
    try
      PropInfo := GetPropInfo(Obj, Elem.Properties[I].Name, [tkInteger, tkEnumeration, tkSet] + tkStrings);
      if (PropInfo <> nil) and (PropInfo^.SetProc <> nil) and not InSkipList(Obj, Elem.Properties[I].Name) then
        case PropInfo^.PropType^.Kind of
          {$IFDEF UNICODE} tkUString, {$ENDIF}
          tkString, tkLString, tkWString:
            SetStrProp(Obj, PropInfo, StringReplace(Elem.Properties[I].Value, cNewline, sLineBreak, []));
          tkSet:
            SetSetProp(Obj, PropInfo, Elem.Properties[I].Value);
          tkEnumeration:
            begin
              S := Elem.Properties[I].Value;
              if (StrToIntDef(S, 0) = 0) and (S <> '0') then
              begin
                try
                  J := GetEnumValue(PropInfo.PropType^, S);
                except
                  J := 0;
                end;
              end
              else
                J := StrToIntDef(S, 0);
              SetOrdProp(Obj, PropInfo, J);
            end;
          tkInteger:
            if PropInfo^.Name = 'ShortCut' then
              SetOrdProp(Obj, PropInfo, TextToShortcut(Elem.Properties[I].Value))
            else
              SetOrdProp(Obj, PropInfo, Elem.Properties[I].IntValue);
        end;
    except
    end;
  end;

  procedure TranslateCollection(const Collection: TCollection; const Elem: TJvSimpleXMLElem);
  var
    I, J: Integer;
  begin
    if Obj = nil then
      Exit;
    for I := 0 to Elem.Items.Count - 1 do
    begin
      J := Elem.Items[I].Properties.IntValue(cIndex, -1);
      if J = -1 then
        Continue;
      if J < Collection.Count then
      begin
        TransProperties(Collection.Items[J], Elem.Items[I]);
        TransObject(Collection.Items[J], Elem.Items[I]);
      end;
    end;
  end;

  procedure TransObject(const Obj: TObject; const Elem: TJvSimpleXMLElem);
  var
    I, J: Integer;
    PropInfo: PPropInfo;
    S: string;
    lObj: TObject;
  begin
    if Obj = nil then
      Exit;
    if IsObject(Obj.ClassType, cTCollection) then
      TranslateCollection(TCollection(Obj), Elem)
    else
      for I := 0 to Elem.Items.Count - 1 do
      try
        PropInfo := GetPropInfo(Obj, Elem.Items[I].Name, [tkInteger, tkEnumeration, tkSet] + tkStrings);
        if (PropInfo <> nil) and (PropInfo^.SetProc <> nil) and not InSkipList(Obj, Elem.Items[I].Name) then
          case PropInfo^.PropType^.Kind of
            {$IFDEF UNICODE} tkUString, {$ENDIF}
            tkString, tkLString, tkWString:
              SetStrProp(Obj, PropInfo, StringReplace(Elem.Items[I].Value, cNewline, sLineBreak, []));
            tkSet:
              SetSetProp(Obj, PropInfo, Elem.Items[I].Value);
            tkEnumeration:
              begin
                S := Elem.Items[I].Value;
                if (StrToIntDef(S, 0) = 0) and (S <> '0') then
                begin
                  try
                    J := GetEnumValue(PropInfo.PropType^, S);
                  except
                    J := 0;
                  end;
                end
                else
                  J := StrToIntDef(S, 0);
                SetOrdProp(Obj, PropInfo, J);
              end;
            tkInteger:
              SetOrdProp(Obj, PropInfo, Elem.Items[I].IntValue);
            tkClass:
              begin
                lObj := GetObjectProp(Obj, Elem.Items[I].Name);
                TransProperties(lObj, Elem.Items[I]);
                TransObject(lObj, Elem.Items[I]);
              end;
          end;
      except
      end;
  end;

begin
  if IsObject(Component.ClassType, cTJvTranslatorStrings) then
  begin
    TransVars;
    Exit;
  end;

  try
    //Transform properties
    if not InSkipList(Component) then
      TransProperties(Component, Elem);

    //Transform childs
    with Component do
      for I := 0 to Elem.Items.Count - 1 do
      begin
        Ok := False;
        for J := 0 to ComponentCount - 1 do
        begin
          S := LowerCase(Elem.Items[I].Name);
          if AnsiSameText(Components[J].Name, S) then
          begin
            TranslateComponent(Components[J], Elem.Items[I]);
            Ok := True;
            Break;
          end;
        end;
        if not Ok then
        begin
          PropInfo := GetPropInfo(Component, Elem.Items[I].Name, [tkUnknown, tkInteger, tkChar, tkEnumeration, tkFloat,
            tkSet, tkClass, tkMethod, tkWChar, tkVariant, tkArray, tkRecord,
            tkInterface, tkInt64, tkDynArray] + tkStrings);
          if (PropInfo <> nil) and (PropInfo^.SetProc <> nil) and not InSkipList(Component, Elem.Items[I].Name) then
          begin
            Obj := GetObjectProp(Component, Elem.Items[I].Name);
            if IsObject(Obj.ClassType, cTStrings) then
              TransStrings(Obj, Elem.Items[I])
            else
            if IsObject(Obj.ClassType, cTTreeNodes) then
              TransTreeNodes(Obj, Elem.Items[I])
            else
            if IsObject(Obj.ClassType, cTListItems) then
              TransListItems(Obj, Elem.Items[I])
            else
            begin
              TransProperties(Obj, Elem.Items[I]);
              TransObject(Obj, Elem.Items[I]);
            end;
          end;
        end;
      end;
  except
  end;
end;

procedure TJvTranslator.Translate(const Form: TCustomForm);
var
  J: Integer;
  S: string;
  lElem: TJvSimpleXMLElem;
begin
  J := Pos('_', Form.Name);
  if J = 0 then
    S := Form.Name
  else
    S := Copy(Form.Name, 1, J - 1);
  lElem := FindItemNamed(nil, S, True);
  if lElem <> nil then
    TranslateComponent(Form, lElem)
end;

function TJvTranslator.Translate(const Category, Item: string): string;
var
  lElem: TJvSimpleXMLElem;
begin
  Result := '';
  lElem := FindItemNamed(nil, Category, True);
  if lElem <> nil then
  begin
    lElem := FindItemNamed(lElem, Item, True);
    if lElem <> nil then
    begin
      Result := lElem.Value;
      if Result = '' then
        Result := lElem.Properties.Value(cValue);
    end;
  end;
end;

procedure TJvTranslator.SkipClass(AClass: TClass);
begin
  SkipProperty(AClass, '');
end;

procedure TJvTranslator.UnskipClass(AClass: TClass);
begin
  UnskipProperty(AClass, '');
end;

function TJvTranslator.InSkipList(AClass: TClass): Boolean;
begin
  Result := InSkipList(AClass, '');
end;

function TJvTranslator.InSkipList(Obj: TObject): Boolean;
begin
  if Obj = nil then
    Result := InSkipList(TObject(nil))
  else
    Result := InSkipList(Obj.ClassType);
end;

function TJvTranslator.InSkipList(Obj: TObject; const PropName: string): Boolean;
begin
  if Obj = nil then
    Result := InSkipList(TObject(nil), PropName)
  else
    Result := InSkipList(Obj.ClassType, PropName);
end;

function TJvTranslator.InSkipList(AClass: TClass; const PropName: string): Boolean;
var
  I: Integer;
  P: PSkipPropRec;
begin
  Result := False;
  if FSkipList <> nil then
    for I := 0 to FSkipList.Count - 1 do
    begin
      P := PSkipPropRec(FSkipList[I]);
      if (P^.AClass = AClass) or AClass.InheritsFrom(P^.AClass) then
      begin
        if ((PropName = '') and (P^.AProps.Count = 0)) or (P^.AProps.IndexOf(PropName) > -1) then
        begin
          Result := True;
          if PropName = '' then
            // move item to beginning of list since it is very likely that we want to access this class very soon
            FSkipList.Move(I, 0);
          Break;
        end;
      end;
    end;
end;

procedure TJvTranslator.Translate(const Stream: TStream; const Form: TCustomForm);
begin
  FXML.LoadFromStream(Stream);
  Translate(Form);
end;

procedure TJvTranslator.TranslateString(const S: string);
var
  Stream: TStringStream;
begin
  Stream := TStringStream.Create(S);
  try
    Translate(Stream);
  finally
    Stream.Free;
  end;
end;

procedure TJvTranslator.TranslateString(const S: string; const Form: TCustomForm);
var
  Stream: TStringStream;
begin
  Stream := TStringStream.Create(S);
  try
    Translate(Stream, Form);
  finally
    Stream.Free;
  end;
end;

procedure TJvTranslator.TranslateScreenString(const S: string);
var
  Stream: TStringStream;
begin
  Stream := TStringStream.Create(S);
  try
    TranslateScreen(Stream);
  finally
    Stream.Free;
  end;
end;

//=== { TJvTranslatorStrings } ===============================================

constructor TJvTranslatorStrings.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FList := THashedStringList.Create;
end;

destructor TJvTranslatorStrings.Destroy;
begin
  FList.Free;
  inherited Destroy;
end;

function TJvTranslatorStrings.Add(const Name: string; var Value: string): Integer;
begin
  // (rom) AddObject? Strange.
  Result := FList.AddObject(Name, TObject(@Value));
end;

function TJvTranslatorStrings.GetString(Index: Integer): string;
begin
  Result := FList[Index];
end;

function TJvTranslatorStrings.IndexOf(const Name: string): Integer;
begin
  Result := FList.IndexOf(Name);
end;

procedure TJvTranslatorStrings.SetString(Index: Integer; const Value: string);
begin
  PString(FList.Objects[Index])^ := Value;
end;

function TJvTranslatorStrings.GetCount: Integer;
begin
  Result := FList.Count;
end;

function TJvTranslatorStrings.GetValue(Index: Integer): string;
begin
  if (Index >= 0) and (Index < Count) and (FList.Objects[Index] <> nil) then
    Result := PString(FList.Objects[Index])^
  else
    Result := '';
end;

procedure TJvTranslator.SkipProperty(AClass: TClass; const PropName: string);
var
  I: Integer;
  P: PSkipPropRec;
begin
  if FSkipList = nil then
    FSkipList := TList.Create;
  for I := 0 to FSkipList.Count - 1 do
    if PSkipPropRec(FSkipList[I])^.AClass = AClass then
    begin
      P := PSkipPropRec(FSkipList[I]);
      if PropName = '' then
        P^.AProps.Clear // skip entire class
      else
      if P^.AProps.Count > 0 then // only add if the class is not skipped as a whole
        P^.AProps.Add(PropName); // the list is sorted, so property name will only be added once
      Exit;
    end;
  // class not found, so add new class record to list
  New(P);
  P^.AClass := AClass;
  P^.AProps := TStringList.Create;
  P^.AProps.Sorted := True;
  if PropName <> '' then
    P^.AProps.Add(PropName); // skip this property only
  FSkipList.Add(P);
  if AClass.InheritsFrom(TPersistent) then
    Classes.RegisterClass(TPersistentClass(AClass));
end;

procedure TJvTranslator.UnskipProperty(AClass: TClass; const PropName: string);
var
  I, J: Integer;
  P: PSkipPropRec;
begin
  if FSkipList <> nil then
  begin
    for I := 0 to FSkipList.Count - 1 do
      if PSkipPropRec(FSkipList[I])^.AClass = AClass then
      begin
        P := PSkipPropRec(FSkipList[I]);
        if PropName <> '' then
          J := P^.AProps.IndexOf(PropName)
        else
        begin
          J := -1;
          P^.AProps.Clear;
        end;
        if J > -1 then
          P^.AProps.Delete(J);
        if P^.AProps.Count = 0 then
          // remove the entry when there are no properties skipped or if this is a UnskipClass call
        begin
          P^.AProps.Free;
          FSkipList.Delete(I);
          Dispose(P);
        end;
        if FSkipList.Count = 0 then
          FreeAndNil(FSkipList);
        Break;
      end;
  end;
end;

procedure TJvTranslator.ClearSkipList;
var
  I: Integer;
begin
  if FSkipList <> nil then
  begin
    for I := 0 to FSkipList.Count - 1 do
    begin
      PSkipPropRec(FSkipList[I]).AProps.Free;
      Dispose(PSkipPropRec(FSkipList[I]));
    end;
    FreeAndNil(FSkipList);
  end;
end;

{$IFDEF UNITVERSIONING}
initialization
  RegisterUnitVersion(HInstance, UnitVersioning);

finalization
  UnregisterUnitVersion(HInstance);
{$ENDIF UNITVERSIONING}

end.
