URL dieses Artikels:

zu Ausgabe: 2.2004
Dolmetscher für Delphi II
Delphi-Anwendungen per VCL-Komponente lokalisieren
von Steffen Rendle
Zur Lokalisierung von Anwendungen gibt es einige Möglichkeiten: Den Delphi Translation-/Localization-Manager, Ressourcen-DLLs oder externe Lösungen. In diesem Artikel werde ich eine Lösung vorstellen, die auf den Delphi Runtime-Type-Informationen basiert. Mit dieser Lösung sind Sie in der Lage mit geringem Aufwand mehrsprachige Anwendungen zu erzeugen - egal ob eine bestehende Anwendung erweitert wird oder Sie eine komplett neue Anwendung erstellen. Selbst nach der Auslieferung der Software werden Ihre Anwender in der Lage sein, neue Sprachen hinzuzufügen oder bestehende anzupassen.

Im Entwickler 2/2002 [1] habe ich einen Weg beschrieben, wie auf einfache Weise mehrsprachige Anwendungen realisiert werden können. Dieser Artikel ist eine Fortsetzung des damaligen Artikels und wird Erweiterungen und den Einsatz der Komponente beschreiben. Für all diejenigen, die den alten Artikel nicht gelesen bzw. mittlerweile vergessen haben, werde ich zu Anfang das Wesentliche wiederholen. Als weitere Themen folgen die Behandlung von Collections, die Sprachsynchronisation zwischen Formularen, Spracherweiterungen nach Auslieferung und die Bereitstellung eines (Sprach-)Editors in der Anwendung.

Wiederholung
Die technische Grundlage der Zuweisung sprachspezifischer Daten sind die Runtime-Type Informationen (kurz RTTI) von Delphi. Über diese lassen sich alle Eigenschaften, die der Objektinspektor kennt, auch zur Laufzeit ohne feste Codierung ansprechen. Jede Klasse (und somit auch deren Instanzen) besitzt eine Informationsstruktur (Zugriff über object.classinfo), die eine Tabelle aller als published deklarierten Eigenschaften enthält. Über getPropInfo(object.classinfo, 'eigenschaft') kann dann auf eine bestimmte Eigenschaft einer Instanz zugegriffen werden. Man bekommt damit unter anderem Zugriff auf den Typ der Eigenschaft (tkInteger, tkString, tkClass, ...) und deren Wert. Das bedeutet, dass man einen Interpreter von Zuweisungen der Form Komponente.Eigenschaft=Wert entwickeln kann. Konkret haben wir damals eine Funktion lngAssignment(root: TComponent; sobject, svalue: String); entwickelt, die dies bewerkstelligt. Es konnten neben Zeichenketten und Zahlen auch Aufzählungstypen, Mengen und geschachtelte Objekte (wie Font) gesetzt werden.


Zur einfachen Handhabung mit Delphi wurde eine TMultilanguage-Komponente entwickelt, die Sprachscripts verwaltet und ausführt. Ein Sprachscript wurde als eine in Abschnitte eingeteilte Sequenz von Zuweisungen definiert. Dabei bekam jede Sprache einen eigenen Abschnitt. Für die bequemen Eingabe der Scripts wurde noch ein eigener Eigenschaftseditor bereitgestellt. Da die Zuweisungsfunktion auf den RTTI basiert, muss man ihr keine Komponenten beibringen, sondern sie kommt mit (fast) allem automatisch zurecht. Das macht sich auch in der Größe der exe-Dateien bemerkbar: Die komplette Bereitstellung der Multilanguage-Funktionalität kostet gerade einmal 9 kByte.

Wichtige Funktionen
Im vorigen Abschnitt wurde erklärt, wie man auf Eigenschaften zugreifen kann, sobald ein Objekt bekannt ist. Eine wichtige Aufgabe ist es dieses Objekt anhand eines gegebenen Strings zu finden. Für die Suche wird bei einem Wurzel-Objekt angefangen und in dessen Informationen rekursiv nach dem im String angegebenen Objekt gesucht. Auf Komponenten-Ebene kann man per component.FindComponent('s') nach einer Komponente suchen; um so tief wie möglich zu suchen (z.B. bei 'form1.button1.font.name' bis 'form1.button1') wird die rekursive Version lngFindComponent verwendet. In unserem Beispiel hilft ab jetzt FindComponent nicht mehr weiter, da font keine Komponente ist. Deswegen muss über die RTTI weiter gesucht werden: getPropInfo gibt in diesem Fall den Typ tkClass zurück, der als Ausgang für die weitere Suche dient. In Listing 1 ist die neue Version von lngGetObject zu finden, die genau diese Suche in den RTTI durchführt. Die komplette Suche (zuerst in den Komponenten und dann in den Objekten) wird von lngFindInstance durchgeführt. Diese Basisfunktionen sind in der Unit MultilanguageUtils.pas zu finden.

Neue Eigenschaftstypen
Ein Manko der alten TMultilanguage-Komponente war, dass sie Array-Eigenschaften wie Columns[i] von TDBGrid nicht ansprechen konnte. Auch Zuweisungen von Komponenten wie listview.Images = imagelist1 waren nicht möglich.


Vor allem die Möglichkeit auf Collections zuzugreifen wird häufig benötigt. Die Implementierung der beiden neuen Eigenschaftstypen ist nicht sehr schwierig. Für die Zuweisung von Komponenten reicht sogar ein Einzeiler: Ist die Eigenschaft vom Typ tkClass, so wird aus dem String, der den Zuweisungswert enthält, ein Objekt gemacht, indem es mit der bekannten Funktion lngFindInstance gesucht wird. Dieses Objekt wird dann über die von Delphi bereitgestellte Prozedur SetObjectProp zugewiesen.


Das Problem der Array-Eigenschaften wie bei einer Collection (Bsp: Columns[i]) ist etwas schwieriger. Zuerst wird anhand der [" "] Klammern in der Befehlszeichenkette festgestellt, dass ein Array angesprochen wird. Es wird also nach dem Objekt (hier Columns) gesucht und dann geschaut, ob dieses vom Typ TCollection ist. Im Erfolgsfall wird das [i] in eine Zahl umgewandelt und (objekt as TCollection).items[i] ist dann das gesuchte Objekt. Auf diesem basierend wird wie gewohnt weitergearbeitet (Listing 1).


Zur Veranschaulichung der verschiedenen Zuweisungsmöglichkeiten habe ich eine Demo geschrieben, die in der Datei objekte.zip zu finden ist. Die Demo zeigt exemplarisch den Zugriff auf die Komponenten DBGrid, MainMenu, MenuItem, ImageList, usw.



Listing 1

...
uses
...typInfo...;
...
function lngGetObject(root: TObject; sobject: String): TObject;
// entspricht component.findcomponent(s) auf Objekt-Basis
var
ppi: PPropInfo;
p, pArray, i, code: Integer;
begin
result := nil;

if root = nil then exit;

// Array-Objekte der Form "object[x]" behandeln...
pArray := pos('[', sobject);
p := length(sobject) + 1;
if (pArray > 0) then
p := pArray;

ppi := getPropInfo(root.classInfo, copy(sobject, 1, p-1));
if ppi <> nil then
case ppi^.PropType^.Kind of
tkClass:
begin
result := GetObjectProp(root, ppi);
// Arrays behandeln...
if pArray = p then
begin
val(copy(sobject, pArray+1, pos(']', sObject)-pArray-1), i, code);
if code = 0 then
begin
// Collections behandeln
if result is TCollection then result := (result as TCollection).Items[i];
end;
end;
end;
end;
end;

Handhabung
Eine weitere häufig gestellte Frage war, wie man mehrere Formulare handhabt. Das einfachste ist hier jedem Formular eine eigene TMultilanguage-Komponente zu spendieren. Für die Synchronisation der Sprachen zwischen den Formularen ist jetzt allerdings etwas Quellcode notwendig. Meist hat man ein Hauptfenster und die anderen Fenster sind diesem untergeordnet. In diesem Fall kann man in dem OnTranslate-Ereignis der Multilanguage-Komponente des Hauptfensters die Sprache der anderen Fenster anpassen. Man setzt einfach:

form2.lng.language = lng.language;
form3.lng.language = lng.language;

Die Sprachumstellung erfolgt dann zentral über die Komponente des Hauptfensters; eine Sprachumstellung in untergeordneten Formularen ist komplizierter. Bei dynamisch erzeugten Formularen muss man beim Erstellen die Spracheigenschaft mit dem Hauptformular synchronisieren.


Zwar bedarf das Synchronisieren zwischen verschiedenen Fenstern wenig Code, aber trotzdem ist das manuelle Eintragen eine lästige und fehleranfällige Arbeit. Deswegen will ich eine Synchronisationskomponente vorstellen, die diese Arbeit abnimmt.

Synchronisationskomponente
Eine automatische Synchronisation ist dank Components-Eigenschaft eines TComponent-Nachfahren sehr einfach zu schreiben. Man durchsucht einfach von Application ausgehend rekursiv alle Components[i] nach TMultilanguage-Herkunft und setzt in diesem Fall die Sprache. Damit ist das Problem gelöst, bei einer Sprachumstellung alle anderen Komponenten zu synchronisieren. Das Setzen der Sprache erfolgt über die TMultilanguageSync-Komponente, welche automatisch die TMultilanguage-Komponenten synchronisiert.


Was ist aber zu tun, wenn ein Fenster zur Laufzeit erstellt wird? In diesem Fall sollte die Sprache des Fensters auch automatisch auf die Sprache der TMultilanguageSync-Komponente gesetzt werden. Das ist mit dem Überschreiben der Loaded-Methode der TMultilanguage-Komponente ebenfalls einfach. Analog zum letzten Fall durchsuchen wir hier wieder die Components von Application rekursiv; diesmal allerdings nach einer TMultilanguageSync-Komponente, deren Sprache übernommen werden soll.


Diese Kombination arbeitet perfekt, falls man die Sprachumstellung immer über die zentrale Synchronisationskomponente durchführt. Man muss also überall dort, wo die Sprache umgestellt werden kann dafür sorgen, dass die zentrale Synchronisationskomponente bekannt ist. Das ist meist machbar, aber falls Fenster applikationsübergreifend in verschiedenen Projekten eingesetzt werden sollen, dann ist es nicht mehr sonderlich praktisch. Der zweite Versuch einer Synchronisationskomponente beseitigt all diese Missstände.

Synchronisation zweiter Versuch
Diesmal gehen wir die Sache anders an: Die Sprache wird nicht mehr in den TMultilanguage-Komponenten selbst gespeichert, sondern nur in der Synchronisationskomponente. Das Setzen der Sprache in einer TMultilanguage-Komponente löst das Setzen der Sprache in der Synchronisationskomponente aus. Diese setzt nur bei sich selbst die Sprache und benachrichtigt alle TMultilanguage-Komponenten, dass sich die Sprache geändert hat. Das veranlasst die Sprachkomponenten dann sich selbst zu übersetzen (d.h. Translate auszuführen). Beim Übersetzen greifen die Sprachkomponenten auf die Sprache der Synchronisationskomponente zurück.


Mit diesem Vorgehen ist eine Synchronisation immer sichergestellt. Man muss der Applikation allerdings immer genau eine Synchronisationskomponente spendieren, da SetLanguage ansonsten keine oder die falsche Wirkung hat. Damit die TMultilanguage-Komponente auch ohne eine einzige Synchronisationskomponente einsatzfähig bleibt, wird die Language-Eigenschaft in der Komponente gespeichert, aber nur gelesen falls keine Synchronisationskomponente existiert. Damit haben wir sowohl die Vorteile der automatischen Synchronisation, als auch die alte Verhaltensweise, falls das gewünscht ist. Selbst zur Laufzeit erstellte Formulare mit TMultilanguage-Komponenten werden ohne zusätzlichen Code automatisch an die richtige Sprache angepasst, da in Loaded von TMultilanguage ein Translate eingetragen ist. Der Quelltext der beiden Komponenten ist auszugsweise in Listing 2 zu sehen.


Zu dem Thema Synchronisation steht wieder eine Demo formulare.zip bereit, die zeigt, wie TMultilanguage mit mehreren Formularen arbeitet. Es werden dabei auch dynamisch erstellte Formulare und deren Synchronisation gezeigt. Dabei ist zu beachten, dass die Synchronisation zwischen Formularen zur Entwurfszeit in der Delphi-IDE nicht funktioniert, da man hier nicht auf Application zugreifen sollte. Application ist in diesem Fall nämlich die Delphi-IDE selbst! Deswegen wird zur Entwurfszeit Multilanguage.Owner (was das Formular ist) als Start für die Suche verwendet.



Listing 2

procedure TMultilanguageSync.SetLanguage(value: String);
// Setzt die Sprache der Synchronisationskomponente und benachrichtigt alle
// TMultilanguage-Komponenten, dass sich die Sprache geändert hat
procedure NotifyMultilanguageComponents(root: TComponent);
var
i: Integer;
begin
for i := 0 to root.ComponentCount-1 do
if (root.Components[i] is TMultiLanguage) then
(root.Components[i] as TMultilanguage).Translate()
else
NotifyMultilanguageComponents(root.Components[i]);
end;

begin
fLanguage := value;
if csDesigning in ComponentState then
NotifyMultilanguageComponents(Owner)
else
NotifyMultilanguageComponents(Application);
end;

function TMultilanguage.SearchMultilanguageSync(root: TComponent):
TMultilanguageSync;
var
i: Integer;
begin
result := nil;
for i := 0 to root.ComponentCount-1 do
if (root.Components[i] is TMultiLanguageSync) then
begin
result := (root.Components[i] as TMultilanguageSync);
exit;
end
else
begin
result := SearchMultilanguageSync(root.Components[i]);
if result <> nil then exit;
end;
end;

function TMultilanguage.GetLanguage: String;
var
Sync: TMultilanguageSync;
begin
if csDesigning in ComponentState then
Sync := SearchMultilanguageSync(Owner)
else
Sync := SearchMultilanguageSync(Application);

if Sync <> nil then
result := Sync.Language
else
result := fLanguage;
end;

procedure TMultilanguage.SetLanguage(value: String);
var
Sync: TMultilanguageSync;
begin
if csDesigning in ComponentState then
Sync := SearchMultilanguageSync(Owner)
else
Sync := SearchMultilanguageSync(Application);

fLanguage := value;

if Sync <> nil then
Sync.Language := value
else
Translate;
end;

Änderung nach Auslieferung
Der größte Vorteil einer skriptbasierenden Übersetzungskomponente besteht darin, dass sowohl die Entwickler als auch die Anwender der Software nach deren Auslieferung in der Lage sind Änderungen vorzunehmen. Kompiliert man das Übersetzungsskript in die Anwendung und bietet keinen Import an, so bleibt dieses Potenzial ungenutzt. Der einfachste Weg des Imports ist es die Sprachskripts der einzelnen Formulare als externe Dateien mitzuliefern und diese dynamisch zu laden. Das ist pro Komponente bereits mit einem Einzeiler machbar:

lng.Script.Loadfromfile('dateiname');


Oft ist es aber ein Mittelweg wünschenswert, d.h. als Hersteller sowohl Standard-Sprachen hinein kompilieren als auch dem Anwender die Möglichkeit geben zusätzliche Sprachen hinzuzufügen. In diesem Fall bietet es sich an, einfach an das Script lng.Script.text die neuen Sprachen anzuhängen. Ein Nebeneffekt, der sowohl erwünscht als auch unerwünscht sein kann, ist dass Benutzer dann die mitgelieferten/ fest-kodierten Sprachen überschreiben können, indem sie einfach noch mal einen Sprachabschnitt mit diesem Sprachbezeichner einfügen. Falls das unerwünscht ist, sollte man vor dem Dazuzuladen die Abschnittsnamen prüfen.


Das Laden von externen Scripts hat einen weiteren Nebeneffekt und zwar, dass die Anwender Zugriff auf alle Komponenten und deren Eigenschaften haben. In manchen Fällen möchte man den Anwendern nicht so viel Macht über die eigene Software geben. Dann sollte man beim Laden der Scripts die Zuweisungen anhand einer positiv oder negativ Liste aussortieren.


Ein Problem bei den bisherigen Sprachskripts ist, dass diese aus der Sicht des Entwicklers entworfen wurden. Aus der Sicht eines Anwenders ist es normalerweise logischer, wenn er nicht für jedes Formular eine eigene Sprachdatei übersetzen muss, sondern pro Sprache eine einzige Datei hat. Ein weiterer Vorteil dieser Veränderung ist es, dass man eine Sprache per Textdatei hinzufügen/ löschen kann, ohne dass man einen Texteditor bemühen muss. Damit lassen sich einzelne Sprachen so auf einer Website anbieten, dass beispielsweise ein spanischer Anwender lediglich die Sprachdatei spanish.lng herunterladen und in einem bestimmten Verzeichnis speichern muss.


Als Realisierung bietet es sich an, die Sprachdatei in verschiedene Abschnitte
mit einem Abschnitt pro TMultilanguage-Komponente (normalerweise eine pro
Formular) einzuteilen (Kasten TMultilanguage). Beim Laden werden die Anweisungen
auf die einzelnen TMultilanguage-Komponenten in den Formularen verteilt.
Diese Aufgabe wird von der Synchronisationskomponente übernommen; wie dies umgesetzt
werden kann, ist in Listing 3 gezeigt.


<table width="450" border="0" cellspacing="0" cellpadding="1">
<tr bgcolor="#000000">
<td>
<table width="100%" border="0" cellspacing="0" cellpadding="4">
<tr bgcolor="#FFFFFF">
<td>

TMultilanguage

Sprachskripts der TMultilanguage-Komponente
für zwei Formulare aus Entwicklersicht:



Datei: form1.lng

[german]

caption = 'Guten Tag'

[english]

caption = 'Hi'



Datei: form2.lng

[german]

caption = 'Sprache: deutsch'

[english]

caption = 'language: english'



Verbesserte, externe Speicherung der Sprachskripts aus Anwendersicht:



Datei: german.lng


[form1.lng]

caption = 'Guten Tag'

[form2.lng]

caption = 'Sprache: deutsch'



Datei: english.lng

[form1.lng]

caption = 'Hi'

[form2.lng]

caption = 'language: english'</td>
</tr>
</table></td>
</tr>
</table>


Listing 3

procedure TMultilanguageSync.LoadLanguage(language: String; script: TStrings);
// verteilt die Abschnitte einer externen Sprachdatei (für die Sprache
// language) auf die einzelnen TMultilanguage-Instanzen
var
i: Integer;
lng: TComponent;
s: String;
lngInstances,
assignments: TStringList;
begin
lngInstances := TStringList.Create;
assignments := TStringList.Create;
try
// die Multilanguage-Instanzen, die angesprochen werden, in
// die Liste lngInstances auslesen...
lngListLanguages(script, lngInstances);

for i := 0 to lngInstances.Count-1 do
begin
// den Zuweisungs-Abschnitt für die i-te Multilanguage-Instanz holen...
lngFilterScript(script, assignments, lngInstances[i]);

// diese Instanze anhand Ihres Namens suchen...
s := lngInstances[i];
lng := lngFindComponent(Application, s);

if (s = '') and (lng is TMultilanguage) then
begin
// falls gefunden die Zuweisungen an das bestehende Script anhängen...
(lng as TMultiLanguage).Script.Text :=
(lng as TMultiLanguage).Script.Text
+ Format(#13#10'[%s]'#13#10, [language])
+ assignments.Text;
end;
end;

finally
assignments.Free;
lngInstances.Free;
end;
end;

Laufzeiteditor
Für die Entwicklungszeit
haben wir in [1] schon einen einfachen Spracheditor bereitgestellt. Nach der Auslieferung
müssen die Anwender bis jetzt aber auf einen normalen Texteditor (z.B. Notepad)
zurückgreifen, falls sie Sprachänderungen (wie im vorigen Abschnitt beschrieben)
durchführen wollen. Das nächstliegende ist es den Editor, der zur Entwicklungszeit
verfügbar ist, auch den Kunden für das Editieren nach der Auslieferung bereitzustellen.
Der Editor wird über die Funktion ExecuteMultiLanguageScriptPE aufgerufen
und übernimmt ein Sprachskript, das er bei Änderungen in der modifizierten Version
zurück liefert. Diese Funktion können Sie z.B. in folgender Form einsetzen:

if ExecuteMultiLanguageScriptPE(lng.Script, Self) then
begin
lng.Translate;
lng.Script.SaveToFile(...);
end;


Für Ihre Kunden kann es unter Umständen empfehlenswert sein, den Editor noch weiter
anzupassen, da die bisherige Version eher einem Entwickler als einem Anwender
entgegenkommt. Die Demo editieren.zip zeigt wie der Eigenschaftseditor
zur Laufzeit verwendet wird und wie externe Sprachdateien zur Laufzeit dazu geladen
werden können.

Eigenschaftseditor
Der in [1] mitgelieferte Eigenschaftseditor für
das Sprachskript kann natürlich auch noch stark verbessert werden. Das größte
Problem an dem Editor ist es, dass man keine Rückmeldungen zum betroffenen Formular
bekommt. Es ist ziemlich lästig sich alle Eigenschaften und Komponentennamen zu
merken, bevor man in den Editor wechselt. Man sollte zumindest direkt im Editor
sehen können, ob eine Komponente existiert oder nicht. Abhilfe verschafft hier
eine Validierungs-Funktion für Komponenten und Eigenschaften. Diese prüfen nach,
ob es eine Komponente mit einer bestimmten Eigenschaft auf dem betroffenen Formular
gibt. Diese Funktion lngObjectExists basiert wieder auf der selben Funktionen
wie lngAssignment aus der MultilanguageUtils-Unit.



Eine zweite große Verbesserung wird durch eine Funktion zum Auslesen von Eigenschaftswerten gegeben. Diese Funktion lngReadValue wird den Wert, den ein Objekt auf dem Formular hat als String zurückgeben. Damit kann man Sprachspezifisches wie gewohnt in dem Delphi-Objektinspektor setzen und kann diese Werte mit einem Klick im Eigenschaftseditor einer Sprache zuweisen. Grundlage für das Auslesen der Werte sind wieder die bisherigen Funktionen aus der MultilanguageUtils-Unit in Verbindung mit den Get...Value-Funktionen aus der typinfo.pas.

Copyright
Eine der häufigsten Fragen betraf die rechtliche Seite. Die Komponente darf natürlich auch in kommerziellen Anwendungen kostenlos eingesetzt werden. Eine Weiterentwicklung/Veränderung ist selbstverständlich ebenfalls gestattet; ansonsten wären die Erklärungen in diesem Artikel zur internen Funktionsweise überflüssig. Weitere Fragen werden im Copyright-Abschnitt des Quellcodes beantwortet. Den Quellcode, alle Demos und Installationshinweise finden Sie auf der Entwickler Profi CD.

Resümee
Ich hoffe, dass Ihnen dieser zweiten Artikel zum Thema mehrsprachige Anwendungen mit Delphi hilft, Ihre Software zu lokalisieren. Mit den vorgestellten Komponenten sollte Ihnen die Entwicklung mehrsprachiger Anwendungen erleichtert werden und Ihren Kunden die Möglichkeit gegeben werden, Ihre Software um zusätzliche Sprachen zu erweitern. Durch die Erläuterungen der einzelnen Komponententeile können Sie TMultilanguage an Ihre individuellen Anforderungen anpassen. Zudem haben Sie nebenbei noch etwas über die interne Funktionsweise von Delphi in Form der RTTI erfahren.



Links und Literatur
  • [1] Steffen Rendle, Dolmetscher für Delphi; Der Entwickler 2/2002

© 2004 Software & Support Verlag GmbH. Vervielfältigung nur mit Genehmigung des Verlags. Fragen?