Lokalisierung: Unterschied zwischen den Versionen
K (→Anforderungen) |
|||
(6 dazwischenliegende Versionen von 6 Benutzern werden nicht angezeigt) | |||
Zeile 3: | Zeile 3: | ||
Als Lokalisierung bezeichnet man den Vorgang eine Software in verschiedene Sprachen zu übersetzen. | 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. | + | 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 == | == Anforderungen == | ||
* Einfach einzusetzen | * Einfach einzusetzen | ||
− | * | + | * Übersetzen in weitere Sprachen ohne Neukompilierung |
* Auch bei anderem Satzbau noch verwendbar | * Auch bei anderem Satzbau noch verwendbar | ||
Zeile 17: | Zeile 17: | ||
== Einfaches Übersetzungssystem == | == Einfaches Übersetzungssystem == | ||
=== Allgemeine Übersetzungen === | === Allgemeine Übersetzungen === | ||
− | Hier werde ich den 3. Ansatz implementieren: | + | Hier werde ich den 3. Ansatz implementieren: Eine Übersetzungsfunktion. |
Ich verwende zur Übersetzung Stringlisten der Form | Ich verwende zur Übersetzung Stringlisten der Form | ||
Bezeichner=Wert | Bezeichner=Wert | ||
− | + | Sie sind also ähnlich wie Ini-Dateien aufgebaut, jedoch ohne Sektionen. | |
− | Sie werden mit Hilfe der Funktion LoadLanguage geladen | + | Sie werden mit Hilfe der Funktion '''LoadLanguage''' geladen: |
− | <pascal> | + | <source lang="pascal">procedure LoadLanguage(const Lang: String); |
− | procedure LoadLanguage(const Lang:String); | ||
begin | begin | ||
− | CurLang:= | + | CurLang:=LowerCase(Lang); |
− | + | LangData.LoadFromFile(ChangeFileExt(ParamStr(0),'.'+CurLang)); | |
− | end; | + | 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 | + | 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. | + | 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> | + | <source lang="pascal">function Translate(const Name: string): string;overload; |
− | + | var i:Integer; | |
− | var i: | ||
begin | begin | ||
− | + | i := LangData.IndexOfName(Name); | |
− | if ( | + | if (I>-1) |
− | then | + | then Result:=LangData.Values[Name]; |
− | else raise | + | 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; | + | end;</source> |
− | </ | ||
− | Diese Funktion | + | 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. | 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: | Ein kleines Beispiel: | ||
− | + | Man hat folgende Übersetzungsdatei: | |
− | Projektname.de | + | Projektname.de |
− | + | startmsg=Herzlich Willkommen | |
− | + | quitmsg=Auf Wiedersehen | |
Dann kann man das im Programm folgendermaßen verwenden: | Dann kann man das im Programm folgendermaßen verwenden: | ||
− | <pascal> | + | <source lang="pascal">procedure TForm1.FormCreate(Sender: TObject); |
− | procedure TForm1.FormCreate(Sender: TObject); | ||
begin | begin | ||
− | + | LoadLanguage('de'); | |
− | + | ShowMessage(Translate('startmsg')); | |
end; | end; | ||
procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction); | procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction); | ||
begin | begin | ||
− | + | ShowMessage(Translate('quitmsg')); | |
− | end; | + | 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 | ||
Diese Funktion hat jedoch den Nachteil nicht für dynamische Texte einsetzbar zu sein. | Diese Funktion hat jedoch den Nachteil nicht für dynamische Texte einsetzbar zu sein. | ||
Zeile 77: | 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> | + | <source lang="pascal">Function Translate(const Name: string; const Args: array of const): String; overload; |
− | Function Translate(const Name: string; const Args: array of const): | ||
begin | begin | ||
− | + | Result:=Format(Translate(Name),Args); | |
− | end; | + | 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 ersetzt bestimmte Zeichenkombinationen im | + | '''Format''' ersetzt bestimmte Zeichenkombinationen im Text durch die Variablen des Arrays: |
%d für Integer | %d für Integer | ||
%s für Strings | %s für Strings | ||
Zeile 92: | Zeile 85: | ||
Auch hierzu ein Beispiel: | Auch hierzu ein Beispiel: | ||
− | Projektname.de | + | Projektname.de |
− | + | attackmsg=Spieler %d greift sie an | |
− | + | chatmsg=<Spieler %d> %s | |
− | Verwendet werden sie dann so | + | Verwendet werden sie dann so |
− | <pascal> | + | <source lang="pascal"> LoadLanguage('de'); |
− | |||
showmessage(translate('attackmsg',[SpielerNummer])); | showmessage(translate('attackmsg',[SpielerNummer])); | ||
− | showmessahe(translate('chatmsg',[SpielerNummer,Nachricht])); | + | 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> | + | <source lang="pascal">function CurrentLanguage:string;</source> |
− | function CurrentLanguage:string; | ||
− | </ | ||
Hier noch der gesamte Quelltext der Übersetzungsunit: | Hier noch der gesamte Quelltext der Übersetzungsunit: | ||
− | <pascal> | + | ''Translator.pas'' |
− | unit Translator; | + | <source lang="pascal">unit Translator; |
interface | 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); | + | procedure LoadLanguage(const Lang: String); |
− | function CurrentLanguage:String; | + | function CurrentLanguage: String; |
implementation | implementation | ||
− | uses | + | uses SysUtils,Classes; |
− | var | + | var LangData:TStringlist; |
CurLang:String; | CurLang:String; | ||
− | Function Translate(const Name: string; const Args: array of const): | + | |
+ | Function Translate(const Name: string; const Args: array of const): String; overload; | ||
begin | begin | ||
− | + | Result:=Format(Translate(Name),Args); | |
end; | end; | ||
− | Function Translate(const Name: string): | + | Function Translate(const Name: string): String; overload; |
− | var i: | + | var i:Integer; |
begin | begin | ||
− | + | i := LangData.IndexOfName(Name); | |
− | if ( | + | if (I>-1) |
− | then | + | then Result:=LangData.Values[Name]; |
− | else raise | + | 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; | end; | ||
procedure LoadLanguage(const Lang:String); | procedure LoadLanguage(const Lang:String); | ||
begin | begin | ||
− | CurLang:= | + | CurLang:=LowerCase(Lang); |
− | + | LangData.LoadFromFile(ChangeFileExt(ParamStr(0),'.'+CurLang)); | |
end; | end; | ||
function CurrentLanguage:String; | function CurrentLanguage:String; | ||
begin | begin | ||
− | + | Result:=CurLang; | |
end; | end; | ||
initialization | initialization | ||
− | + | LangData:=TStringList.Create; | |
− | + | LangData.Sorted:=true; | |
finalization | finalization | ||
− | + | LangData.Free; | |
− | end. | + | end.</source> |
− | </ | + | |
=== Übersetzung von Formularen und Komponenten === | === Übersetzung von Formularen und Komponenten === | ||
− | Im Gegensatz zu Translator.pas die relativ allgemein einsetzbar ist, ist diese darauf aufbauende | + | Im Gegensatz zu ''Translator.pas'' die relativ allgemein einsetzbar ist, ist diese darauf aufbauende Unit auf die Delphi VCL spezialisiert. |
− | Sie stellt | + | Sie stellt zwei weitere Funktionen zu Verfügung: |
− | Zum einen eine weitere Überladung von | + | Zum einen eine weitere Überladung von '''Translate''', die ein ganzes Formular auf einmal übersetzt |
− | <pascal> | + | <source lang="pascal">Procedure Translate(const Component:TComponent;Path:String='');overload;</source> |
− | Procedure Translate(const Component:TComponent;Path:String='');overload; | ||
− | </ | ||
Verwendung: | Verwendung: | ||
− | <pascal> | + | <source lang="pascal">Translate(Form1)</source> |
− | Translate(Form1) | ||
− | </ | ||
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> | + | <source lang="pascal">procedure CreateTranslationTable(const Component:TComponent;const Filename:String);</source> |
− | procedure CreateTranslationTable(const Component:TComponent;const Filename:String); | ||
− | </ | ||
Verwendung: | Verwendung: | ||
− | <pascal> | + | <source lang="pascal">CreateTranslationTable(Form1,'Form1table.txt');</source> |
− | 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: | 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 199: | Zeile 181: | ||
Form1.a.Hint=>>a<< | Form1.a.Hint=>>a<< | ||
Form1.Action1.Caption=Hallo | Form1.Action1.Caption=Hallo | ||
− | Form1.Action1.Hint= | + | Form1.Action1.Hint=Hallo und Willkommen |
Dabei werden folgende Eigenschaften gespeichtert/gelesen: | 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<br>Das funktioniert beispielsweise bei | + | * 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<br>Das funktioniert beispielsweise bei Label, Edit, Memo, Panel ... |
* TCombobox/TListbox: Die Eigenschaft Items | * TCombobox/TListbox: Die Eigenschaft Items | ||
* TTabControl: Die Eigenschaft Tabs | * TTabControl: Die Eigenschaft Tabs | ||
− | <pascal> | + | ''TranslatorVCL.pas'' |
− | unit TranslatorVCL; | + | <source lang="pascal">unit TranslatorVCL; |
interface | interface | ||
Zeile 224: | Zeile 206: | ||
else Path:=Path+'.'+Component.Name; | else Path:=Path+'.'+Component.Name; | ||
//Eigenschaften übersetzen | //Eigenschaften übersetzen | ||
− | if (Component is TControl)and(TMyControl(Component).Caption<>'')then TMyControl(Component).Caption:=Translate(Path+'.Caption'); | + | if (Component is TControl)and |
− | if (Component is TControl)and(TControl(Component).Hint<>'')then TMyControl(Component).Hint:=Translate(Path+'.Hint'); | + | (TMyControl(Component).Caption<>'')then |
− | if (Component is TMenuItem)and(TMenuItem(Component).Caption<>'')and(TMenuItem(Component).Action=nil)then TMenuItem(Component).Caption:=Translate(Path+'.Caption'); | + | TMyControl(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 TControl)and |
− | if (Component is TCustomAction)and(TCustomAction(Component).Caption<>'')then TCustomAction(Component).Caption:=Translate(Path+'.Caption'); | + | (TControl(Component).Hint<>'')then |
− | if (Component is TCustomAction)and(TCustomAction(Component).Hint<>'')then TCustomAction(Component).Hint:=Translate(Path+'.Hint'); | + | TMyControl(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 TMenuItem)and |
− | if (Component is TCustomComboBox)and(TCustomComboBox(Component).items.text<>'')then TCustomComboBox(Component).items.text:=Translate(Path+'.Items'); | + | (TMenuItem(Component).Caption<>'')and |
− | if (Component is TCustomListBox)and(TCustomListBox(Component).items.text<>'')then TCustomListBox(Component).items.text:=Translate(Path+'.Items'); | + | (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 | //Unterkomponenten übersetzen | ||
for i:=0 to Component.ComponentCount-1do | for i:=0 to Component.ComponentCount-1do | ||
Zeile 256: | Zeile 258: | ||
else Path:=Path+'.'+Component.Name; | else Path:=Path+'.'+Component.Name; | ||
//Eigenschaften speichern | //Eigenschaften speichern | ||
− | + | if (Component is TControl)and | |
− | if (Component is TControl)and(TControl(Component).Hint<>'')then List.add(Path+'.Hint='+Escape(TMyControl(Component).Hint)); | + | (TMyControl(Component).Caption<>'')then |
− | if (Component is TMenuItem)and(TMenuItem(Component).Caption<>'')and(TMenuItem(Component).Action=nil)then List.add(Path+'.Caption='+Escape(TMenuItem(Component).Caption)); | + | List.add(Path+'.Caption='+Escape(TMyControl(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 TControl)and |
− | if (Component is TCustomAction)and(TCustomAction(Component).Caption<>'')then List.add(Path+'.Caption='+Escape(TCustomAction(Component).Caption)); | + | (TControl(Component).Hint<>'')then |
− | if (Component is TCustomAction)and(TCustomAction(Component).Hint<>'')then List.add(Path+'.Hint='+Escape(TCustomAction(Component).Hint)); | + | List.add(Path+'.Hint='+Escape(TMyControl(Component).Hint)); |
− | if (Component is TTabControl)and(TTabControl(Component).Tabs.text<>'')then List.add(Path+'.Tabs='+Escape(TTabControl(Component).Tabs.text)); | + | if (Component is TMenuItem)and |
− | if (Component is TCustomComboBox)and(TCustomComboBox(Component).Items.text<>'')then List.add(Path+'.Items='+Escape(TCustomComboBox(Component).Items.text)); | + | (TMenuItem(Component).Caption<>'')and |
− | if (Component is TCustomListBox)and(TCustomListBox(Component).Items.text<>'')then List.add(Path+'.Items='+Escape(TCustomListBox(Component).Items.text)); | + | (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 | //Unterkomponenten speichern | ||
for i:=0 to Component.ComponentCount-1do | for i:=0 to Component.ComponentCount-1do | ||
Zeile 279: | Zeile 301: | ||
end; | end; | ||
− | end. | + | end.</source> |
− | </ | + | {{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
Inhaltsverzeichnis
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.
Beide units können frei in beliebigen Projekten verwendet und angepasst werden. |