Lokalisierung: Unterschied zwischen den Versionen

Aus DGL Wiki
Wechseln zu: Navigation, Suche
(Übersetzung von Formularen und Komponenten: Alte Codeformatierung sprengte Seitendimensionen)
K (Anforderungen)
 
(2 dazwischenliegende Versionen von 2 Benutzern werden nicht angezeigt)
Zeile 7: Zeile 7:
 
== Anforderungen ==
 
== Anforderungen ==
 
* Einfach einzusetzen
 
* Einfach einzusetzen
* Software kann auch nach dem Übersetzen noch verändert werden
+
* Übersetzen in weitere Sprachen ohne Neukompilierung
 
* Auch bei anderem Satzbau noch verwendbar
 
* Auch bei anderem Satzbau noch verwendbar
  
Zeile 23: Zeile 23:
  
 
Sie werden mit Hilfe der Funktion '''LoadLanguage''' geladen:
 
Sie werden mit Hilfe der Funktion '''LoadLanguage''' geladen:
<pascal>procedure LoadLanguage(const Lang: String);
+
<source lang="pascal">procedure LoadLanguage(const Lang: String);
 
begin
 
begin
 
  CurLang:=LowerCase(Lang);
 
  CurLang:=LowerCase(Lang);
 
  LangData.LoadFromFile(ChangeFileExt(ParamStr(0),'.'+CurLang));
 
  LangData.LoadFromFile(ChangeFileExt(ParamStr(0),'.'+CurLang));
end;</pascal>
+
end;</source>
  
 
Diese Funktion macht nicht mehr als die momentane Sprache in einer Variable zu speichern und die Übersetzungsdatei zu laden. Der Dateiname wird bei mir über '''ChangeFileExt(ParamStr(0),'.'+CurLang)''' festgelegt, das solltet ihr jedoch an euer Projekt anpassen.
 
Diese Funktion macht nicht mehr als die momentane Sprache in einer Variable zu speichern und die Übersetzungsdatei zu laden. Der Dateiname wird bei mir über '''ChangeFileExt(ParamStr(0),'.'+CurLang)''' festgelegt, das solltet ihr jedoch an euer Projekt anpassen.
Zeile 33: Zeile 33:
 
Die eigentliche Übersetzung wird von der Funktion '''Translate''' durchgeführt.
 
Die eigentliche Übersetzung wird von der Funktion '''Translate''' durchgeführt.
 
(Ich bevorzuge aussagekräftige Funktionsnamen, besonders im C/C++ Bereich habe ich auch schon einen einfachen Unterstrich als Namen für so eine Funktion gesehen. Das ist auch in Delphi möglich, falls ihr so tippfaul seid)
 
(Ich bevorzuge aussagekräftige Funktionsnamen, besonders im C/C++ Bereich habe ich auch schon einen einfachen Unterstrich als Namen für so eine Funktion gesehen. Das ist auch in Delphi möglich, falls ihr so tippfaul seid)
<pascal>function Translate(const Name: string): string;overload;
+
<source lang="pascal">function Translate(const Name: string): string;overload;
 
var i:Integer;
 
var i:Integer;
 
begin
 
begin
Zeile 43: Zeile 43:
 
  Result:=StringReplace(result,'\n',#10,[rfReplaceAll]);
 
  Result:=StringReplace(result,'\n',#10,[rfReplaceAll]);
 
  Result:=StringReplace(result,'\\','\',[rfReplaceAll]);
 
  Result:=StringReplace(result,'\\','\',[rfReplaceAll]);
end;</pascal>
+
end;</source>
  
 
Diese Funktion sucht den über den Parameter ''Name'' übergebenen Bezeichner in der Stringlist (ich verwende auf Performancegründen eine sortierte Stringlist und die Funktion Find) wenn sie ihn findet werden noch die Escapezeichen \r (Wagenrücklauf=#13) und \n (Neue Zeile=#10) sowie der Doppelbackslash ersetzt und das Ergebnis zurückgeliefert.
 
Diese Funktion sucht den über den Parameter ''Name'' übergebenen Bezeichner in der Stringlist (ich verwende auf Performancegründen eine sortierte Stringlist und die Funktion Find) wenn sie ihn findet werden noch die Escapezeichen \r (Wagenrücklauf=#13) und \n (Neue Zeile=#10) sowie der Doppelbackslash ersetzt und das Ergebnis zurückgeliefert.
Zeile 56: Zeile 56:
  
 
Dann kann man das im Programm folgendermaßen verwenden:
 
Dann kann man das im Programm folgendermaßen verwenden:
<pascal>procedure TForm1.FormCreate(Sender: TObject);
+
<source lang="pascal">procedure TForm1.FormCreate(Sender: TObject);
 
begin
 
begin
 
  LoadLanguage('de');
 
  LoadLanguage('de');
Zeile 65: Zeile 65:
 
begin
 
begin
 
  ShowMessage(Translate('quitmsg'));
 
  ShowMessage(Translate('quitmsg'));
end;</pascal>
+
end;</source>
 
Es ist jedoch darauf zu achten, dass dass '''Translate''' case-sensitive ist, also '''Translate('StartMsg')''' nicht das selbe wie '''Translate('startmsg')''' ist.
 
Es ist jedoch darauf zu achten, dass dass '''Translate''' case-sensitive ist, also '''Translate('StartMsg')''' nicht das selbe wie '''Translate('startmsg')''' ist.
  
Zeile 72: Zeile 72:
  
 
Daher gibt es noch eine zweite erweiterte Übersetzungsfunktion die auf die erste zurückgreift:
 
Daher gibt es noch eine zweite erweiterte Übersetzungsfunktion die auf die erste zurückgreift:
<pascal>Function Translate(const Name: string; const Args: array of const): String; overload;
+
<source lang="pascal">Function Translate(const Name: string; const Args: array of const): String; overload;
 
begin
 
begin
 
  Result:=Format(Translate(Name),Args);
 
  Result:=Format(Translate(Name),Args);
end;</pascal>
+
end;</source>
 
Diese Funktion übernimmt zusammen mit dem Bezeichner noch ein Array an Variablen/Konstanten, die es dann zusammen mit der Übersetzung des Bezeichners an die Delphifunktion '''Format''' übergibt.
 
Diese Funktion übernimmt zusammen mit dem Bezeichner noch ein Array an Variablen/Konstanten, die es dann zusammen mit der Übersetzung des Bezeichners an die Delphifunktion '''Format''' übergibt.
  
Zeile 90: Zeile 90:
  
 
Verwendet werden sie dann so
 
Verwendet werden sie dann so
<pascal> LoadLanguage('de');
+
<source lang="pascal"> LoadLanguage('de');
 
  showmessage(translate('attackmsg',[SpielerNummer]));
 
  showmessage(translate('attackmsg',[SpielerNummer]));
  showmessahe(translate('chatmsg',[SpielerNummer,Nachricht]));</pascal>
+
  showmessahe(translate('chatmsg',[SpielerNummer,Nachricht]));</source>
  
 
Zur Abfrage welche Sprache momentan aktiv ist dient: (Ihr Ergebnis ist klein geschrieben)
 
Zur Abfrage welche Sprache momentan aktiv ist dient: (Ihr Ergebnis ist klein geschrieben)
<pascal>function CurrentLanguage:string;</pascal>
+
<source lang="pascal">function CurrentLanguage:string;</source>
  
 
Hier noch der gesamte Quelltext der Übersetzungsunit:
 
Hier noch der gesamte Quelltext der Übersetzungsunit:
 
''Translator.pas''
 
''Translator.pas''
<pascal>unit Translator;
+
<source lang="pascal">unit Translator;
 
interface
 
interface
  
Zeile 147: Zeile 147:
 
finalization
 
finalization
 
  LangData.Free;
 
  LangData.Free;
end.</pascal>
+
end.</source>
  
 
=== Übersetzung von Formularen und Komponenten ===
 
=== Übersetzung von Formularen und Komponenten ===
Zeile 154: Zeile 154:
 
Sie stellt zwei weitere Funktionen zu Verfügung:
 
Sie stellt zwei weitere Funktionen zu Verfügung:
 
Zum einen eine weitere Überladung von '''Translate''', die ein ganzes Formular auf einmal übersetzt
 
Zum einen eine weitere Überladung von '''Translate''', die ein ganzes Formular auf einmal übersetzt
<pascal>Procedure Translate(const Component:TComponent;Path:String='');overload;</pascal>
+
<source lang="pascal">Procedure Translate(const Component:TComponent;Path:String='');overload;</source>
 
Verwendung:
 
Verwendung:
<pascal>Translate(Form1)</pascal>
+
<source lang="pascal">Translate(Form1)</source>
  
 
und eine weitere Funktion die eine ensprechende Übersetzungsdatei aus einem Formular generiert:
 
und eine weitere Funktion die eine ensprechende Übersetzungsdatei aus einem Formular generiert:
<pascal>procedure CreateTranslationTable(const Component:TComponent;const Filename:String);</pascal>
+
<source lang="pascal">procedure CreateTranslationTable(const Component:TComponent;const Filename:String);</source>
 
Verwendung:
 
Verwendung:
<pascal>CreateTranslationTable(Form1,'Form1table.txt');</pascal>
+
<source lang="pascal">CreateTranslationTable(Form1,'Form1table.txt');</source>
 
Dabei entsteht eine Datei die etwa wie die folgende aussieht, und dann vom Benutzer übersetzt und in die jeweilige Sprachdatei integriert werden sollte:
 
Dabei entsteht eine Datei die etwa wie die folgende aussieht, und dann vom Benutzer übersetzt und in die jeweilige Sprachdatei integriert werden sollte:
 
  Form1.Caption=Form1
 
  Form1.Caption=Form1
Zeile 189: Zeile 189:
  
 
''TranslatorVCL.pas''
 
''TranslatorVCL.pas''
<pascal>unit TranslatorVCL;
+
<source lang="pascal">unit TranslatorVCL;
  
 
interface
 
interface
Zeile 301: Zeile 301:
 
end;
 
end;
  
end.</pascal>
+
end.</source>
 
{{Hinweis|Beide units können frei in beliebigen Projekten verwendet und angepasst werden.}}
 
{{Hinweis|Beide units können frei in beliebigen Projekten verwendet und angepasst werden.}}
 +
 +
[[Kategorie:Technik oder Algorithmus]]

Aktuelle Version vom 2. Januar 2014, 00:23 Uhr

Lokalisierung

Allgemein

Als Lokalisierung bezeichnet man den Vorgang eine Software in verschiedene Sprachen zu übersetzen.

Besonders schwer ist die Übersetzung in Sprachen mit komplett anderem Zeichensatz (Kyrillisch, Chinesisch, Japanisch etc.), insbesondere wenn diese Multibyte-Schriften benötigen. So weit wird jedoch kaum ein Hobbyentwickler gehen, der aus keinem dieser Länder stammt, weshalb ich darauf nicht eingehen werde.

Anforderungen

  • Einfach einzusetzen
  • Übersetzen in weitere Sprachen ohne Neukompilierung
  • Auch bei anderem Satzbau noch verwendbar

Ansätze

  • Formularresourcen (Borland)
    Nachteil: Es ist schwer das Formular nach dem Übersetzen noch anzupassen und es wird bei sonstigen Texten keine Unterstützung geboten
  • Programm das im Quelltext alle Strings suchen und ersetzen kann
    Nachteil: Es gibt viele nicht zu übersetzende Strings, Programm muss für jede Sprache kompiliert werden
  • Übersetzungsfunktion:
    Vorteil: Kann überall im Quelltext eingesetzt werden
    Nachteil: Jeder zu übersetzende String muss an diese Funktion übergeben werden

Einfaches Übersetzungssystem

Allgemeine Übersetzungen

Hier werde ich den 3. Ansatz implementieren: Eine Übersetzungsfunktion. Ich verwende zur Übersetzung Stringlisten der Form

Bezeichner=Wert

Sie sind also ähnlich wie Ini-Dateien aufgebaut, jedoch ohne Sektionen.

Sie werden mit Hilfe der Funktion LoadLanguage geladen:

procedure LoadLanguage(const Lang: String);
begin
 CurLang:=LowerCase(Lang);
 LangData.LoadFromFile(ChangeFileExt(ParamStr(0),'.'+CurLang));
end;

Diese Funktion macht nicht mehr als die momentane Sprache in einer Variable zu speichern und die Übersetzungsdatei zu laden. Der Dateiname wird bei mir über ChangeFileExt(ParamStr(0),'.'+CurLang) festgelegt, das solltet ihr jedoch an euer Projekt anpassen.

Die eigentliche Übersetzung wird von der Funktion Translate durchgeführt. (Ich bevorzuge aussagekräftige Funktionsnamen, besonders im C/C++ Bereich habe ich auch schon einen einfachen Unterstrich als Namen für so eine Funktion gesehen. Das ist auch in Delphi möglich, falls ihr so tippfaul seid)

function Translate(const Name: string): string;overload;
var i:Integer;
begin
 i := LangData.IndexOfName(Name);
 if (I>-1)
  then Result:=LangData.Values[Name];
  else raise Exception.Create('String not translated: "'+Name+'"');
 Result:=StringReplace(result,'\r',#13,[rfReplaceAll]);
 Result:=StringReplace(result,'\n',#10,[rfReplaceAll]);
 Result:=StringReplace(result,'\\','\',[rfReplaceAll]);
end;

Diese Funktion sucht den über den Parameter Name übergebenen Bezeichner in der Stringlist (ich verwende auf Performancegründen eine sortierte Stringlist und die Funktion Find) wenn sie ihn findet werden noch die Escapezeichen \r (Wagenrücklauf=#13) und \n (Neue Zeile=#10) sowie der Doppelbackslash ersetzt und das Ergebnis zurückgeliefert. Wenn der Bezeichner nicht gefunden wird, wird eine Exception generiert. Dieses Verhalten ist zum Debuggen nützlich, sollte im Endprodukt jedoch möglichst geändert werden, wenn man den Benutzer nicht mit Exceptions verärgern will.

Ein kleines Beispiel: Man hat folgende Übersetzungsdatei:

Projektname.de
 startmsg=Herzlich Willkommen
 quitmsg=Auf Wiedersehen

Dann kann man das im Programm folgendermaßen verwenden:

procedure TForm1.FormCreate(Sender: TObject);
begin
 LoadLanguage('de');
 ShowMessage(Translate('startmsg'));
end;

procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction);
begin
 ShowMessage(Translate('quitmsg'));
end;

Es ist jedoch darauf zu achten, dass dass Translate case-sensitive ist, also Translate('StartMsg') nicht das selbe wie Translate('startmsg') ist.

Diese Funktion hat jedoch den Nachteil nicht für dynamische Texte einsetzbar zu sein. z.B. wenn ich den Text 'Spieler '+SpielerNummer+' greift dich an' übersetzen will, bräuchte ich für jede Spielernummer einen eigenen Bezeichner, was sicher nicht effizient ist, und bei Spielernamen komplett versagen würde.

Daher gibt es noch eine zweite erweiterte Übersetzungsfunktion die auf die erste zurückgreift:

Function Translate(const Name: string; const Args: array of const): String; overload;
begin
 Result:=Format(Translate(Name),Args);
end;

Diese Funktion übernimmt zusammen mit dem Bezeichner noch ein Array an Variablen/Konstanten, die es dann zusammen mit der Übersetzung des Bezeichners an die Delphifunktion Format übergibt.

Format ersetzt bestimmte Zeichenkombinationen im Text durch die Variablen des Arrays: %d für Integer %s für Strings %f für Kommazahlen Weitere Informationen dazu bietet die Delphi-Hilfe unter "Format-Strings"

Auch hierzu ein Beispiel:

Projektname.de
 attackmsg=Spieler %d greift sie an
 chatmsg=<Spieler %d> %s

Verwendet werden sie dann so

 LoadLanguage('de');
 showmessage(translate('attackmsg',[SpielerNummer]));
 showmessahe(translate('chatmsg',[SpielerNummer,Nachricht]));

Zur Abfrage welche Sprache momentan aktiv ist dient: (Ihr Ergebnis ist klein geschrieben)

function CurrentLanguage:string;

Hier noch der gesamte Quelltext der Übersetzungsunit: Translator.pas

unit Translator;
interface

function Translate(const Name: String; const Args: array of const): String; overload;
function Translate(const Name: String): String; overload;

procedure LoadLanguage(const Lang: String);
function CurrentLanguage: String;

implementation
uses SysUtils,Classes;

var LangData:TStringlist;
    CurLang:String;

Function Translate(const Name: string; const Args: array of const): String; overload;
begin
 Result:=Format(Translate(Name),Args);
end;

Function Translate(const Name: string): String; overload;
var i:Integer;
begin
 i := LangData.IndexOfName(Name);
 if (I>-1)
  then Result:=LangData.Values[Name];
  else raise Exception.Create('String not translated: "'+Name+'"');
 Result:=StringReplace(result,'\r',#13,[rfReplaceAll]);
 Result:=StringReplace(result,'\n',#10,[rfReplaceAll]);
 Result:=StringReplace(result,'\\','\',[rfReplaceAll]);
end;

procedure LoadLanguage(const Lang:String);
begin
 CurLang:=LowerCase(Lang);
 LangData.LoadFromFile(ChangeFileExt(ParamStr(0),'.'+CurLang));
end;

function CurrentLanguage:String;
begin
 Result:=CurLang;
end;

initialization
 LangData:=TStringList.Create;
 LangData.Sorted:=true;
finalization
 LangData.Free;
end.

Übersetzung von Formularen und Komponenten

Im Gegensatz zu Translator.pas die relativ allgemein einsetzbar ist, ist diese darauf aufbauende Unit auf die Delphi VCL spezialisiert. Sie stellt zwei weitere Funktionen zu Verfügung: Zum einen eine weitere Überladung von Translate, die ein ganzes Formular auf einmal übersetzt

Procedure Translate(const Component:TComponent;Path:String='');overload;

Verwendung:

Translate(Form1)

und eine weitere Funktion die eine ensprechende Übersetzungsdatei aus einem Formular generiert:

procedure CreateTranslationTable(const Component:TComponent;const Filename:String);

Verwendung:

CreateTranslationTable(Form1,'Form1table.txt');

Dabei entsteht eine Datei die etwa wie die folgende aussieht, und dann vom Benutzer übersetzt und in die jeweilige Sprachdatei integriert werden sollte:

Form1.Caption=Form1
Form1.Label1.Caption=Label1
Form1.CheckBox1.Caption=CheckBox1
Form1.RadioButton1.Caption=RadioButton1
Form1.GroupBox1.Caption=GroupBox1
Form1.RadioGroup1.Caption=RadioGroup1
Form1.Panel1.Caption=Panel1
Form1.BitBtn1.Caption=BitBtn1
Form1.BitBtn1.Hint=Klick Me
Form1.TabControl1.Tabs=a\r\nb\r\nc\r\n
Form1.TabSheet1.Caption=TabSheet1
Form1.Memo1.Caption=a\r\nb\r\nc\r\n
Form1.ListBox1.Items=a\r\nb\r\nc\r\n
Form1.ComboBox1.Caption=ComboBox1
Form1.ComboBox1.Items=a\r\nb\r\nc\r\n
Form1.a.Caption=a
Form1.a.Hint=>>a<<
Form1.Action1.Caption=Hallo
Form1.Action1.Hint=Hallo und Willkommen

Dabei werden folgende Eigenschaften gespeichtert/gelesen:

  • Bei allen von TControl abgeleiteten Komponenten sowie TMenuItem und TCustomAction die Eigenschaften Caption/Text und Hint sofern sie im ursprünglichen Formular den Wert hat
    Das funktioniert beispielsweise bei Label, Edit, Memo, Panel ...
  • TCombobox/TListbox: Die Eigenschaft Items
  • TTabControl: Die Eigenschaft Tabs

TranslatorVCL.pas

unit TranslatorVCL;

interface
uses classes,translator,controls,menus,actnlist,sysutils,comctrls,stdctrls;
Procedure Translate(const Component:TComponent;Path:String='');overload;
procedure CreateTranslationTable(const Component:TComponent;const Filename:String);
implementation
Type TMyControl=class(TControl);

Procedure Translate(const Component:TComponent;Path:String='');overload;
var i:integer;
begin
 //Pfad anpassen
 if Path=''
  then Path:=Component.Name
  else Path:=Path+'.'+Component.Name;
 //Eigenschaften übersetzen
 if (Component is TControl)and
    (TMyControl(Component).Caption<>'')then 
    TMyControl(Component).Caption:=Translate(Path+'.Caption');
 if (Component is TControl)and
    (TControl(Component).Hint<>'')then 
    TMyControl(Component).Hint:=Translate(Path+'.Hint');
 if (Component is TMenuItem)and
    (TMenuItem(Component).Caption<>'')and
    (TMenuItem(Component).Action=nil)then 
    TMenuItem(Component).Caption:=Translate(Path+'.Caption');
 if (Component is TMenuItem)and
    (TMenuItem(Component).Hint<>'')and
    (TMenuItem(Component).Action=nil)then 
    TMenuItem(Component).Hint:=Translate(Path+'.Hint');
 if (Component is TCustomAction)and
    (TCustomAction(Component).Caption<>'')then 
    TCustomAction(Component).Caption:=Translate(Path+'.Caption');
 if (Component is TCustomAction)and
    (TCustomAction(Component).Hint<>'')then 
    TCustomAction(Component).Hint:=Translate(Path+'.Hint');
 if (Component is TTabControl)and
    (TTabControl(Component).Tabs.text<>'')then 
    TTabControl(Component).Tabs.text:=Translate(Path+'.Tabs');
 if (Component is TCustomComboBox)and
    (TCustomComboBox(Component).items.text<>'')then 
    TCustomComboBox(Component).items.text:=Translate(Path+'.Items');
 if (Component is TCustomListBox)and
    (TCustomListBox(Component).items.text<>'')then 
    TCustomListBox(Component).items.text:=Translate(Path+'.Items');
 //Unterkomponenten übersetzen
 for i:=0 to Component.ComponentCount-1do
  Translate(Component.Components[i],Path);
end;

procedure CreateTranslationTable(const Component:TComponent;const Filename:String);
var List:TStringlist;
   function Escape(const S:String):String;
   begin
    result:=S;
    result:=stringreplace(result,'\','\\',[rfReplaceAll]);
    result:=stringreplace(result,#13,'\r',[rfReplaceAll]);
    result:=stringreplace(result,#10,'\n',[rfReplaceAll]);
   end;

   procedure AddComponent(const Component:TComponent;Path:String);
   var i:integer;
   begin
    //Pfad anpassen
    if Path=''
     then Path:=Component.Name
     else Path:=Path+'.'+Component.Name;
    //Eigenschaften speichern
        if (Component is TControl)and
       (TMyControl(Component).Caption<>'')then 
       List.add(Path+'.Caption='+Escape(TMyControl(Component).Caption));
    if (Component is TControl)and
       (TControl(Component).Hint<>'')then 
       List.add(Path+'.Hint='+Escape(TMyControl(Component).Hint));
    if (Component is TMenuItem)and
       (TMenuItem(Component).Caption<>'')and
       (TMenuItem(Component).Action=nil)then 
       List.add(Path+'.Caption='+Escape(TMenuItem(Component).Caption));
    if (Component is TMenuItem)and
       (TMenuItem(Component).Hint<>'')and
       (TMenuItem(Component).Action=nil)then 
       List.add(Path+'.Hint='+Escape(TMenuItem(Component).Hint));
    if (Component is TCustomAction)and
       (TCustomAction(Component).Caption<>'')then 
       List.add(Path+'.Caption='+Escape(TCustomAction(Component).Caption));
    if (Component is TCustomAction)and
       (TCustomAction(Component).Hint<>'')then 
       List.add(Path+'.Hint='+Escape(TCustomAction(Component).Hint));
    if (Component is TTabControl)and
       (TTabControl(Component).Tabs.text<>'')then 
       List.add(Path+'.Tabs='+Escape(TTabControl(Component).Tabs.text));
    if (Component is TCustomComboBox)and
       (TCustomComboBox(Component).Items.text<>'')then 
       List.add(Path+'.Items='+Escape(TCustomComboBox(Component).Items.text));
    if (Component is TCustomListBox)and
       (TCustomListBox(Component).Items.text<>'')then 
       List.add(Path+'.Items='+Escape(TCustomListBox(Component).Items.text));
    //Unterkomponenten speichern
    for i:=0 to Component.ComponentCount-1do
     AddComponent(Component.Components[i],Path);
   end;
begin
 List:=TStringlist.create;
 try
  AddComponent(Component,'');
  List.SaveToFile(Filename);
 finally
  List.free;
 end;
end;

end.
Info DGL.png Beide units können frei in beliebigen Projekten verwendet und angepasst werden.