Главная страница
    Top.Mail.Ru    Яндекс.Метрика
Форум: "Corba";
Текущий архив: 2009.02.08;
Скачать: [xml.tar.bz2];

Вниз

Обращение к удаленном OLE-объекту   Найти похожие ветки 

 
GrBob   (2006-06-27 11:07) [0]

Здравствуйте.
На удаленном компьютере есть OLE-объект, у которого известно только название. (ну и соответственно легко получается CLSID, в реестре на данном компьютере информация об объекте есть). Вопрос: можно ли как-нибудь к нему обраться? CreateOLEObject работает только на текущей машине, а CreateRemoteComObject возвращает интерфейс, название которого я не знаю. (Как его получить?)
Заранее спасибо за ответы.


 
tesseract ©   (2006-06-27 14:57) [1]


> а CreateRemoteComObject возвращает интерфейс, название которого
> я не знаю. (Как его получить?)


enuminterfaces??

описание интерфейса есть?


 
GrBob   (2006-06-27 15:15) [2]


> enuminterfaces??


??


> описание интерфейса есть?


Я знаю его методы и свойства. Мне нужно узнать как он называется. (I...)


 
tesseract ©   (2006-06-28 13:56) [3]

Если есть GUID - на кой его по имени знать?


 
GrBob   (2006-06-28 16:47) [4]

Дело в том, что мне его нужно запустить на удаленной машине. CreateOLEObject делает это только на локальной, все замечательно работает. А вот CreateRemoteCOMObject возвращает интерфейс, который нужно чему-то присвоить. Просто IInterface"у его не присвоишь, вернее потом методы не повызываешь.
Согласен, возможно интерфейс знать и не нужно совсем, но тогда как запустить OLE сервер на удаленном компьютере и еще им управлять при этом? :)


 
AbrosimovA ©   (2006-06-29 15:53) [5]

unit Unit1;

interface

uses
 Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
 Dialogs, StdCtrls, ComObj, AxCtrls, ExtCtrls, ComCtrls;

type
 TForm1 = class(TForm)
   btnReadProp: TButton;
   ListBox1: TListBox;
   ServNameEdit: TEdit;
   Label1: TLabel;
   Label4: TLabel;
   btnExec: TButton;
   Timer1: TTimer;
   ValuePropEdit: TEdit;
   btnReadInterf: TButton;
   btnConnect: TButton;
   BtnRemConnect: TButton;
   StatusBar1: TStatusBar;
   procedure btnReadPropClick(Sender: TObject);
   procedure FormClose(Sender: TObject; var Action: TCloseAction);
   procedure btnExecClick(Sender: TObject);
   procedure ListBox1Click(Sender: TObject);
   procedure Timer1Timer(Sender: TObject);
   procedure FormCreate(Sender: TObject);
   procedure btnReadInterfClick(Sender: TObject);
   procedure BtnRemConnectClick(Sender: TObject);
 private
  ServerName: string[255];
  procedure SetInvoke(const ID: integer; const Command: OleVariant);
 public
 end;

var
 Form1: TForm1;
 Srv: IDispatch;
 ID,DID: integer;
 iIndex: integer;

implementation

{$R *.dfm}
uses ActiveX;

//Перечисление свойств
procedure EnumProperties(Dispatch: IDispatch; Entries: TStrings);
var iDispID,i: integer;
   NameRefs: array[0..0] of PWideChar;
   HR: HResult;
begin
Entries.Clear;
Entries.NameValueSeparator:="-";
EnumDispatchProperties(Dispatch, GUID_NULL, VT_EMPTY, Entries);
for i:=0 to Entries.Count-1 do begin
  NameRefs[0]:=PWideChar(WideString(Entries[i]));
  HR:=Dispatch.GetIDsOfNames(GUID_NULL, @NameRefs, 1,
    LOCALE_SYSTEM_DEFAULT, @iDispID);
  if HR=S_OK then
   Entries.Strings[i]:= Format("%d-%s",[iDispID,Entries[i]]);
end
end;

//Перечисление интерфейсов
procedure EnumInterface(Dispatch: IDispatch; Entries: TStrings; Functions: Boolean);
var
 TypeInfo: ITypeInfo;
 TypeAttr: PTypeAttr;
 FuncDesc: PFuncDesc;
 Vardesc: PVarDesc;
 hr: HResult;
 iMethode: Integer;
 nNames  : integer;
 sNameLst: TBSTRList;
begin
 Dispatch.GetTypeInfoCount(iMethode);
 if iMethode > 0 then
 begin
   hr := Dispatch.GetTypeInfo(0,GetUserDefaultLCID,TypeInfo);
   OleCheck(hr);
   hr := TypeInfo.GetTypeAttr(TypeAttr);
   OleCheck(hr);
   if Functions then
   begin
     for iMethode := 0 to TypeAttr.cFuncs-1 do
     begin
       hr := TypeInfo.GetFuncDesc(iMethode, FuncDesc);
       OleCheck(hr);
       hr := TypeInfo.GetNames(FuncDesc.memid, @sNameLst,1,nNames);
       OleCheck(hr);
       Entries.Add(Format("%d-%s", [FuncDesc.memid, sNameLst[0]]));
     end;
   end
   else
   begin
     for iMethode := 0 to TypeAttr.cVars-1 do
     begin
       hr := TypeInfo.GetVarDesc(iMethode, Vardesc);
       OleCheck(hr);
       hr := TypeInfo.GetNames(Vardesc.memid, @sNameLst,1,nNames);
       OleCheck(hr);
       Entries.Add(Format("%d-%s", [Vardesc.memid, sNameLst[0]]));
     end;
   end;
 end;
end;

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
Srv:=nil;
end;

function GetDispIDFromName(const Disp: IDispatch; const PropertyName: WideString;
           var iDispID: Integer): OleVariant;
var
PName: PWideChar;
ExcepInfo: TExcepInfo;
DispParams: TDispParams;
Status: HResult;
begin
if Disp = nil then Exit;
PName := PWideChar(PropertyName);
if PropertyName = "" then
  Result := DISPID_UNKNOWN
else
  Disp.GetIDsOfNames(GUID_NULL, @PName, 1, GetThreadLocale, @iDispID);
FillChar(DispParams, SizeOf(DispParams), 0);

Status := Disp.Invoke(iDispID, GUID_NULL, 0, DISPATCH_PROPERTYGET, DispParams,
  @Result, @ExcepInfo, nil);
if Status <> S_OK then
  DispatchInvokeError(Status, ExcepInfo);
end;

procedure TForm1.SetInvoke(const ID: integer; const Command: OleVariant);
var
 vSet        : OLEVariant;
 aDispParams : TDispParams;
 aDispId     : TDispId;
 aEI         : TExcepInfo;
 iError      : UINT;
 ptinfo: ITypeInfo;
begin
 vSet := Command;
 FillChar(aDispParams, SizeOf (aDispParams), 0);
 with aDispParams do begin
   rgvarg := @vSet;
   cArgs := 1;
   cNamedArgs := 1;
 end;
 aDispId := DISPID_PROPERTYPUT;
 aDispParams.rgdispidNamedArgs := @aDispId;
 OleCheck (Srv.Invoke (ID, GUID_NULL, LOCALE_SYSTEM_DEFAULT,
           DISPATCH_PROPERTYPUT, aDispParams, NIL, @aEI, @iError));
end;

procedure TForm1.ListBox1Click(Sender: TObject);
begin
iIndex:=Listbox1.ItemIndex;
if (iIndex<>-1)and(Tag=1) then
 ValuePropEdit.Text:=GetDispIDFromName(Srv, Listbox1.Items.ValueFromIndex[iIndex],DID)
end;

procedure TForm1.Timer1Timer(Sender: TObject);
begin
btnExec.Enabled:=(iIndex<>-1)and(Tag=1);
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
iIndex:=-1;
end;

//интерфейсы
procedure TForm1.btnReadInterfClick(Sender: TObject);
begin
Tag:=2;
ListBox1.Clear;
ValuePropEdit.Clear;
if Assigned(Srv) then EnumInterface(Srv,ListBox1.Items,True)
end;

//свойства
procedure TForm1.btnReadPropClick(Sender: TObject);
begin
 Tag:=1;
 ListBox1.Clear;
 ValuePropEdit.Clear;
 if Assigned(Srv) then begin
  EnumProperties(Srv,ListBox1.Items);
 end;
end;

//Изменение свойств
procedure TForm1.btnExecClick(Sender: TObject);
begin
if (ValuePropEdit.Text = "")or(Tag=2) then Exit;
try
 SetInvoke(DID, ValuePropEdit.Text);
except
end
end;

//Запуск  
procedure TForm1.BtnRemConnectClick(Sender: TObject);
var CLSID: TGUID;
begin
 //для Word.Application
 CLSID:=StringToGUID("{000209FF-0000-0000-C000-000000000046}");
 try
  Srv:=CreateRemoteComObject("Server5",CLSID) as IDispatch;
 except
  Exit;
 end;
 btnReadProp.Enabled:=True;
 btnReadInterf.Enabled:=True;
end;

end.


 
GrBob   (2006-06-30 09:24) [6]

Огромное спасибо, тут я более-менее разобрался, осталось узнать как вызывать методы, а не только менять свойства.
PS. Методы возвращают другие COM-объекты.
Жду с большим нетерпением подсказки, наконец я хоть немного продвинулся.


 
AbrosimovA ©   (2006-06-30 12:40) [7]


> осталось узнать как вызывать методы

Так как, я проект давно забросил, то в этом направлении я не копал.
Но думаю, в Invoke нужно использовать DISPATCH_METHOD и правильно заполнить DispParams


 
Медвед   (2006-07-03 10:20) [8]

нужно написать прокси-объект, реализующий IDispatch
пример есть в исходниках Borland Socket Server


 
GrBob   (2006-07-06 15:54) [9]

Мне кажется, что писать для этого дополнительный объект - все же перебор. Продолжая разбираться со стандартным IDispatch"ем и его Invoke"ом я смог вызвать методы без параметров. С параметрами все намного грустнее. Нашел вот такой кусок кода:
var O : IDispatch;
 Disp: TDispParams;
 Args: array[0..2] of TVariantArg;
begin
 O := CreateOleObject("Project1.Test");

 // Присваиваю аргументы
 OleVariant(Args[0]) := 1;
 OleVariant(Args[1]) := 2;
 OleVariant(Args[2]) := 3;

 with Disp do begin
   rgvarg := @Args;
   cArgs := 3;
   rgdispidNamedArgs := nil;
   cNamedArgs := 0;
 end;

 O.Invoke(1,GUID_NULL,GetThreadLocale,DISPATCH_METHOD,Disp,nil,nil,nil);
end;


Я и сам пытался написать что-то подобное (естественно, уходя от CreateOLEObject), но он падает на  преобразовании TVariantArg"a к ОлеВарианту. В МСДН вроде написано, что должно работать, да и у автора этого кода тоже все работало. У меня же - Invalid Variant type. Подскажите, в чем дело?


 
AbrosimovA ©   (2006-07-06 16:54) [10]


> OleVariant(Args[0]) := 1;  OleVariant(Args[1]) := 2;  OleVariant(Args[2])
> := 3;


Так ли уж необходимо преобразовывать к олевариантному типу?


 
GrBob   (2006-07-06 17:09) [11]

Да, необходимо. Но я еще поковырялся и нашел немного другой способ:

procedure TForm1.Button2Click(Sender: TObject);
var
 DispParams: TDispParams;
egin
 FillChar(DispParams, SizeOf (DispParams), 0);
 DispParams.cArgs := 2;
 GetMem(DispParams.rgvarg, DispParams.cArgs * SizeOf(TVariantArg));
 try
   DispParams.rgvarg[0] := TVariantArg(OleVariant("aaa"));
   DispParams.rgvarg[1] := TVariantArg(OleVariant("bbb"));
   Exec(Self.Caption, DispParams);
 finally
   FreeMem(DispParams.rgvarg, DispParams.cArgs * SizeOf(TVariantArg));
 end;
end;


Функция Exec потом ищет метод, вызывает его ну и т.д., не суть важно. Вроде работает, но надо еще проверять, почему то иногда падает (возможно даже не в этом месте :)).


 
Ломброзо ©   (2006-07-07 02:10) [12]

Может я чего-то недопонимаю, но OleVariant устроен таким образом, что в том случае, если переменная типа OleVariant содержит указатель на интерфейс IDispatch, то все вызовы к "свойствам" и "методам" этой переменной транслируются в вызов IDispatch::Invoke(). Так что пользоваться IDispatch совершенно излишне. То бишь тот указатель на интерфейс, который был отмаршаллен с другого хоста, можно смело присвоить переменной типа OleVariant и вызывать всякие свойства и методы. Компилятор с разруливанием вызовов расправится сам.

Более того. Если эта библиотека, содержащая определение коклассов и интерфейсов, написана как положено и позволяет получить IDL, ничто не мешает импортировать библиотеку типов, сгенерировать из неё паскалевские заглушки и включить в проект.


 
GrBob   (2006-07-07 08:07) [13]

Предположим, так:
CLSID := ProgIDToClassID("Bla.Bla");
try
 RemoteComp := ServNameEdit.Text;
 Srv := CreateRemoteComObject(RemoteComp, CLSID) as IDispatch;
 OleVariant(Srv).Start;
except
 Exit;
end;

Получаю при вызове метода Start "Разрушительный сбой". Да и написана эта библиотека так, что библиотеки типов просто нет в списке импортируемых.


 
AbrosimovA ©   (2006-07-07 08:10) [14]


> Ломброзо ©   (07.07.06 02:10) [12]
> можно смело присвоить переменной типа OleVariant и вызывать
> всякие свойства и методы


Если только тебе заранее известны все методы и свойства исследуемого интерфейса.


 
GrBob   (2006-07-07 09:06) [15]


> AbrosimovA ©   (07.07.06 08:10) [14]
> Если только тебе заранее известны все методы и свойства
> исследуемого интерфейса.

Дак они мне известны (по крайней мере те, которые я хочу использовать), почему-то не работает :(

В общем вот такие вот первые итоги: привожу на примере службы индексирования, она описана в MSDN и стоит на Windows Server 2003 по умолчанию, да и не главное это :) Просто у Ворда слишком заумные методы, чтобы на нем тесты ставить:

unit Unit1;

interface

uses
 Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
 Dialogs, ComCtrls, ExtCtrls, StdCtrls, ActiveX;

type
 TForm1 = class(TForm)
   ListBox1: TListBox;
   ServNameEdit: TEdit;
   Label4: TLabel;
   ValuePropEdit: TEdit;
   btnReadInterf: TButton;
   BtnRemConnect: TButton;
   StatusBar1: TStatusBar;
   Button1: TButton;
   AddCatalog: TButton;
   GroupBox1: TGroupBox;
   Label1: TLabel;
   Edit1: TEdit;
   Label2: TLabel;
   Edit2: TEdit;
   GroupBox2: TGroupBox;
   Start: TButton;
   Button4: TButton;
   procedure BtnRemConnectClick(Sender: TObject);
   procedure FormClose(Sender: TObject; var Action: TCloseAction);
   procedure FormCreate(Sender: TObject);
   procedure Button1Click(Sender: TObject);
   procedure AddCatalogClick(Sender: TObject);
   procedure StartClick(Sender: TObject);
 private
   procedure Invoke(const ID: integer; const DispParams: TDispParams);
   procedure SetInvoke(const ID: integer);
   procedure Exec(const Name: WideString; const DispParams: TDispParams);
 public
 end;

var
 Form1: TForm1;
 Srv: IDispatch;
 DID: integer;
 iIndex: integer;

implementation

{$R *.dfm}
uses ComObj, AxCtrls;

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
 Srv := nil;
end;

procedure TForm1.SetInvoke(const ID: integer);
var
 aDispParams: TDispParams;
 aEI: TExcepInfo;
 iError: UINT;
 Command: OleVariant;
begin
 FillChar(aDispParams, SizeOf (aDispParams), 0);
 aDispParams.cArgs := 1;
 Command := True;
 GetMem(aDispParams.rgvarg, aDispParams.cArgs * SizeOf(TVariantArg));
 aDispParams.rgvarg[0] := TVariantArg(OleVariant("C:\"));
 aDispParams.rgvarg[1] := TVariantArg(OleVariant("D:\"));
 OleCheck(Srv.Invoke(ID, GUID_NULL, 0,
   DISPATCH_PROPERTYPUT, aDispParams, NIL, @aEI, @iError));
end;

procedure TForm1.Invoke(const ID: integer; const DispParams: TDispParams);
var
 aDispParams: TDispParams;
 aEI: TExcepInfo;
 iError: UINT;
begin
 aDispParams := DispParams;
 OleCheck(Srv.Invoke(ID, GUID_NULL, 0,
   DISPATCH_METHOD, aDispParams, NIL, @aEI, @iError));
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
 iIndex := -1;
end;

procedure TForm1.BtnRemConnectClick(Sender: TObject);
var
 CLSID: TGUID;
 RemoteComp: WideString;
begin
 CLSID := ProgIDToClassID("Microsoft.IsAdm");
 try
   RemoteComp := ServNameEdit.Text;
   Srv := CreateRemoteComObject(RemoteComp, CLSID) as IDispatch;
 except
   Exit;
 end;
end;

procedure TForm1.Button1Click(Sender: TObject);
var
 PName: PWideChar;
 iDispID: Integer;
 Name : WideString;
begin
 iIndex := Listbox1.ItemIndex;
 ListBox1.Items.NameValueSeparator := "-";
 Name := Listbox1.Items.ValueFromIndex[iIndex];
 PName := PWideChar(Name);
 if Srv.GetIDsOfNames(GUID_NULL, @PName, 1, GetThreadLocale, @iDispID) = S_OK then
   SetInvoke(iDispID);
end;

procedure TForm1.Exec(const Name: WideString; const DispParams: TDispParams);
var
 PName: PWideChar;
 iDispID: Integer;
begin
 PName := PWideChar(Name);
 Srv.GetIDsOfNames(GUID_NULL, @PName, 1, GetThreadLocale, @iDispID);
 Invoke(iDispID, DispParams);
end;

procedure TForm1.AddCatalogClick(Sender: TObject);
var
 DispParams: TDispParams;
 Catalog, Place: WideString;
begin
 FillChar(DispParams, SizeOf (DispParams), 0);
 DispParams.cArgs := 2;
 GetMem(DispParams.rgvarg, DispParams.cArgs * SizeOf(TVariantArg));
 try
   Catalog := Edit1.Text;
   Place := Edit2.Text;
   DispParams.rgvarg[0] := TVariantArg(OleVariant(Place));
   DispParams.rgvarg[1] := TVariantArg(OleVariant(Catalog));
   Exec("AddCatalog", DispParams);
 finally
   FreeMem(DispParams.rgvarg, DispParams.cArgs * SizeOf(TVariantArg));
 end;
end;

procedure TForm1.StartClick(Sender: TObject);
var
 DispParams: TDispParams;
begin
 FillChar(DispParams, SizeOf (DispParams), 0);
 DispParams.cArgs := 0;
 GetMem(DispParams.rgvarg, DispParams.cArgs * SizeOf(TVariantArg));
 try
   Exec("Start", DispParams);
 finally
   FreeMem(DispParams.rgvarg, DispParams.cArgs * SizeOf(TVariantArg));
 end;
end;

end.


Что дальше - ума не приложу. Инвоке возвращает очень содержательную ошибку с текстом "Ошибка" :( В чем может быть дело?


 
Ломброзо ©   (2006-07-07 11:38) [16]

> Инвоке возвращает очень содержательную ошибку с текстом "Ошибка"
Скопируйте эту вашу библиотеку на свою машину и зарегистрируйте её через regsvr32


 
GrBob   (2006-07-07 11:51) [17]


> Скопируйте эту вашу библиотеку на свою машину и зарегистрируйте
> её через regsvr32

Она у меня зарегестрирована :) На своей машине прекрасно работает, а эта ошибка выдается даже если указать в качестве удаленной машины - свою.


 
Ломброзо ©   (2006-07-07 12:13) [18]

Да я так, на всякий случай.
Продолжаем разбираться дальше )
Как обстоят дела с настройками DCOM и правами?


 
GrBob   (2006-07-07 12:24) [19]

Права - я администратор как на своей, так и на удаленной (в данном случае опять же своей :) ) машине. Что необходимо настроить в DCOM?


 
Ломброзо ©   (2006-07-07 12:27) [20]

Зайти в оснастку "Component Services" (на XP)->Свойства компьютера или dcomcnfg, проверить опцию, разрешающую использование DCOM.


 
GrBob   (2006-07-07 12:37) [21]

Аааа, ну это то конечно установлено :)


 
AbrosimovA ©   (2006-07-07 14:44) [22]

Проверено на методе Add интерфейса Documents объекта Word.Application - работает.

var iSrv: IDispatch;
    DID: integer;
    Value: OleVariant;

DID:=StrToInt(ListView2.Selected.Caption);
Value:=GetDispIDFromName(iSrv, ListView2.Items[iIndex].SubItems[0], DID);
case VarType(Value) of
 varDispatch:
   begin              
     //Connect;
     ISrv:=IDispatch(Value);
   end;
 varEmpty:
   begin
     //Exec;
     try        
       SetInvokeMethod(iSrv,DID);
     except
       on E: Exception do begin
         StatusBar1.SimpleText:=E.Message;
       end
     end
   end;
end;

procedure SetInvokeMethod(const Dispatch: IDispatch; const ID: integer);
var
DispParams: TDispParams;
Args: array[0..1] of TVariantArg;
ExcepInfo   : TExcepInfo;
iError      : UINT;
begin
Args[0].vt:= VT_ERROR;
Args[0].scode := DISP_E_PARAMNOTFOUND;
Args[1].vt:= VT_ERROR;
Args[1].scode := DISP_E_PARAMNOTFOUND;
FillChar(DispParams, SizeOf (DispParams), 0);
with DispParams do begin
  rgvarg := @Args;
  cArgs := 2;
  cNamedArgs := 0;
end;
OleCheck (Dispatch.Invoke(ID, GUID_NULL, LOCALE_SYSTEM_DEFAULT,
                 DISPATCH_METHOD, DispParams, nil, @ExcepInfo, @iError));
end;


 
AbrosimovA ©   (2006-07-07 16:54) [23]

Не могу найти ответа на следующий вопрос:

Как программно узнать, что нужно передавать через DispParams для безошибочного выполнения методов  через IDispatch.Invoke


 
имя   (2006-07-09 20:53) [24]

Удалено модератором


 
GrBob   (2006-07-11 12:31) [25]


> Как программно узнать, что нужно передавать через DispParams
> для безошибочного выполнения методов  через IDispatch.Invoke

Советую взглянуть в сторону интерфейсов ITypeLib и ITypeInfo. Сам еще не разобрался, но имхо должно подойти. Мне как раз сейчас это тоже понадобилось, просто времени совсем нет читать, если разберешься, отпишись :)


 
AbrosimovA ©   (2006-07-11 14:27) [26]

Это дополнение к коду из 5-го поста для процедуры
procedure EnumInterface(Dispatch: IDispatch; Entries: TStrings; Functions: Boolean); :

type TArgInfo = record
 Total: integer;
 Optional: integer;
 ArgsType: array[0..100] of TVARTYPE;
end;

var ArgInfo: array of TArgInfo;

SetLength(ArgInfo,TypeAttr.cFuncs);
//Общее число параметров метода
ArgInfo[iMethod].Total:=FuncDesc.cParams;
//Число необязательных параметров из них
ArgInfo[iMethod].Optional:=FuncDesc.cParamsOpt;параметров
//типы всех параметров
for i:=0 to FuncDesc.cParams-1 do           ArgInfo[iMethod].ArgsType[i]:=TVARTYPE(FuncDesc.lprgelemdescParam[i].tdesc.vt);< /CODE>



Страницы: 1 вся ветка

Форум: "Corba";
Текущий архив: 2009.02.08;
Скачать: [xml.tar.bz2];

Наверх





Память: 0.55 MB
Время: 0.006 c
15-1229069695
Slider007
2008-12-12 11:14
2009.02.08
С днем рождения ! 12 декабря 2008 пятница


1-1207490509
mufan
2008-04-06 18:01
2009.02.08
private|public разделы для программно создаваемой формы


2-1229934605
Alexxxx
2008-12-22 11:30
2009.02.08
вопрос sendmessage


2-1230112638
J.T
2008-12-24 12:57
2009.02.08
BorderIcons


2-1230394998
Te
2008-12-27 19:23
2009.02.08
!!!





Afrikaans Albanian Arabic Armenian Azerbaijani Basque Belarusian Bulgarian Catalan Chinese (Simplified) Chinese (Traditional) Croatian Czech Danish Dutch English Estonian Filipino Finnish French
Galician Georgian German Greek Haitian Creole Hebrew Hindi Hungarian Icelandic Indonesian Irish Italian Japanese Korean Latvian Lithuanian Macedonian Malay Maltese Norwegian
Persian Polish Portuguese Romanian Russian Serbian Slovak Slovenian Spanish Swahili Swedish Thai Turkish Ukrainian Urdu Vietnamese Welsh Yiddish Bengali Bosnian
Cebuano Esperanto Gujarati Hausa Hmong Igbo Javanese Kannada Khmer Lao Latin Maori Marathi Mongolian Nepali Punjabi Somali Tamil Telugu Yoruba
Zulu
Английский Французский Немецкий Итальянский Португальский Русский Испанский